Martin Hinshelwood

A Scottish software developer: SSW Solution Architect, Microsoft Visual Studio ALM MVP & Scrum Developer Trainer

  Home  |   Contact  |   Syndication    |   Login
  408 Posts | 10 Stories | 576 Comments | 57 Trackbacks

News

www.SSW.com.auSSW was the first company in the world outside of Microsoft to deploy Visual Studio 2010 Team Foundation Server to production, not once, but twice.

Team Foundation Server

Visual Studi2010 ALM SSW provides expert Visual Studio ALM guidance including installation, configuration and customisation through our four Microsoft Visual Studio ALM MVP’s in three countries; Australia, Beijing and the UK. They have experience deploying to small development shops all the way through to large blue chips.

Call us on +(44) 141 416 0993 or +(61) 2 9953 3000 to get started!

Professional Scrum Developer Training

Professional Scrum Developer Training SSW has six Professional Scrum Developer Trainers who specialise in training your developers in implementing Scrum with Microsoft's Visual Studio ALM tools.

Call us on +(44) 141 416 0993 or +(61) 2 9953 3000 to get started!

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

Personal








Locations of visitors to this page

Twitter












Tag Cloud


Article Categories

Archives

Post Categories

Image Galleries

Blogs I read

Blogs of Friends

Personal

Projects

VSTS

Tuesday, March 09, 2010 #

Well, it depends….

If you are a small company that creates a finite number of internal projects then you will find it easier to create a single project for each of your products and have TFS do the heavy lifting with reporting, SharePoint sites and Version Control.

But what if you are not…

Update 9th March 2010

  • Michael Fourie gave me some feedback which I have integrated.
  • Ed Blankenship via @edblankenship offered encouragement and a nice quote.
  • Ewald Hofman gave me a couple of Cons, and maybe a few more soon. Ewald’s company, Avanade, currently uses Areas, but it looks like the manual management is getting too much and the project is getting cluttered.

Updated 11th March 2010

  • Adam Cogan – Adam suggested I get our disagreement out in the open, improve the proposed solution description with some visual cues and move the Pros and Cons to the top. Last but not least, to plug out custom TFS template :)

 


 

What if you are likely to have hundreds of projects, possibly with a multitude of internal and external projects? You might have 1 project for a customer or 10. This is the situation that most consultancies find themselves in and thus they need a more sustainable and maintainable option. What I am advocating is that we should have 1 “Team Project” per customer, and use areas to create “sub projects” within that single “Team Project”.

"What you describe is what we generally do internally and what we recommend. We make very heavy use of area path to categorize the work within a larger project."
- Brian Harry, Microsoft Technical Fellow & Product Unit Manager for Team Foundation Server

This post has an ulterior motive as I am having this debate with my boss, Adam Cogan, and we both decided that we wanted to find out what the community at large thinks of this approach to managing projects in TFS. Adam thinks this is a bad idea as it is not supported “out-of-the-box”, and I think that a lot of things are not supported “out-of-the-box” in TFS which never the less, are a good idea, including this one.

"We tend to use areas to segregate multiple projects in the same team project and it works well."
- Tiago Pascoal, Visual Studio ALM MVP

 

"In general, I believe this approach provides consistency [to multi-product engagements] and lowers the administration and maintenance costs. All good."
- Michael Fourie, Visual Studio ALM MVP

 

@MrHinsh BTW, I'm very much a fan of very large, if not huge, team projects in TFS. Just FYI :) Use Areas & Iterations.”
Ed Blankenship, Visual Studio ALM MVP

 

I am proposing that SSW change from over 70 internal team projects:

  • SSW.CodeAuditor
  • SSW.SQLAuditor
  • SSW.SQLDeploy
  • etc

To 1 internal team project:

  • SSW
    • CodeAuditor
    • SQLAuditor
    • SQLDeploy
    • etc

Note: The single Team Project called “SSW” would contain all of our internal projects and consequently all of the Areas and Iteration move down one hierarchy to accommodate this. Where we would have had “\SSW\Sprint 1” we now have “\SSW\SqlDeploy\Sprint1” with “SqlDeploy” being our internal project. At the moment SSW has over 70 internal projects and more than 170 total projects in TFS.

This method has long term benefits that help to simplify the support model for companies that often have limited internal support time and many projects. But, there are implications as TFS does not provide this model “out-of-the-box”. These implications stretch across Areas, Iterations, Queries, Project Portal and Version Control.

Michael made a good comment, he said:

I agree with your approach, assuming that in a multi-product engagement with a client, they are happy to adopt the same process template across all products. If they are not, then it’ll either be easy to convince them or there is a valid reason for having a different template
- Michael Fourie, Visual Studio ALM MVP

 

At SSW we have a standard SSW Agile process template that we use and this is applied across the board, to all of our projects. We even apply any changes to the core process template to all of our existing projects as well. If you have multiple projects for the same clients on multiple templates and you want to keep it that way, then this approach will not work for you. However, if you want to standardise as we have at SSW then this approach may benefit you as well.

Pros

  • You only have one project to upgrade when a process template changes – After going through an upgrade of over 170 project prior to the changes in the RC I can tell you that that many projects is no fun.
  • Standardises your Process Template – You will always have the same Process implementation across projects/products without exception
  • You get tighter control over the permissions – Yes, you can do this on a standard Team Project, but it gets a lot easier with practice.
  • You can “move” work items from one “product” to another – Have we not always wanted to do that.
  • You can rename your projects – Wahoo: everyone wants to do this, now you can.
  • One set of Reporting Services reports to manage – You set an area and iteration to run reports anyway, so you may as well set both.
  • Simplified Check-In Policies– There is only one set of check-in policies per client. This simplifies administration of policies.
  • Simplified Alerts – As alerts are applied across multiple projects this simplifies your alert rules as per client.

Cons

All of these cons could be mitigated by a custom tool that helps automate creation of “Sub-projects” within Team Projects. This custom tool could create areas, Iteration, permissions, SharePoint and queries. It just does not exist yet :)

  • You need to configure the Areas and Iterations – This is just like you would do for Sprints/Iterations and for functional areas of your application, but with 1 extra level at the top of the tree.
  • You need to configure the permissions – This I guess is the main configuration point. It is possible to create the same permissions as a Team Project at this level, but that would be a bit of configuration work.
  • You may need to configure sub sites for SharePoint (depends on your requirement) – If you have two projects/products in the same Team Project then you will not see the burn down for each one out-of-the-box, but rather a cumulative for the Team Project. This is not really that much of a problem as you would have to configure your burndown graphs for your current iteration anyway.
    note: When you create a sub site to a TFS linked portal it will inherit the settings of its parent site :) This is fantastic as it means that you can easily create sub sites and then set the Area and Iteration path in each of the reports to be the correct one.
  • Every team wants their own customization (via Ewald Hofman) - small teams of 2 persons against teams of 30 – or even outsourcing – need their own process, you cannot allow that because everybody gets the same work item types.
    note: Luckily at SSW this is not a problem as our template is standardised across all projects and customers.
  • Large list of builds (via Ewald Hofman) – As the build list in Team Explorer is just a flat list it can get very cluttered.
    note: I would mitigate this by removing any build that has not been run in over 30 days. The build template and workflow will still be available in version control, but it will clean the list.

Implications around Areas

Areas should be used for topological classification/isolation of work items. You can think of this as architecture areas, organisational areas or even the main features of your application. In our scenario there is an additional top level item that represents the Project / Product that we want to chop our Team Project into.

image
Figure: Creating a sub area to represent a product/project is easy.

<teamproject>

<teamproject>\<Functional Area/module whatever>

Becomes:

<teamproject>

<teamproject>\<ProjectName>\

<teamproject>\<ProjectName>\<Functional Area/module whatever>

Implications around Iterations

Iterations should be used for chronological classification/isolation of work items. This could include isolated time boxes, milestones or release timelines and really depends on the logical flow of your project or projects. Due to the new level in Area we need to add the same level to Iteration. This is primarily because it is unlikely that the sprints in each of your projects/products will start and end at the same time. This is just a reality of managing multiple projects.

image
Figure: Adding the same Area value to Iteration as the top level item adds flexibility to Iteration.

<teamproject>\Sprint 1

Or

<teamproject>\Release 1\Sprint 1

Becomes:

<teamproject>\<ProjectName>\Sprint 1

Or

<teamproject>\<ProjectName>\Release 1\Sprint 1

Implications around Queries

Queries are used to filter your work items based on a specified level of granularity. There are a number of queries that are built into a project created using the MSF Agile 5.0 template, but we now have multiple projects and it would be a pain to have to edit all of the work items every time we changed project, and that would only allow one team to work on one project at a time.

image 
Figure: The Queries that are created in a normal MSF Agile 5.0 project do not quite suit our new needs.

In order for project contributors to be able to query based on their project we need a couple of things. The first thing I did was to create an “_Area Template” folder that has a copy of the project layout with all the queries setup to filter based on the “_Area Template” Area and the “_Sprint template” you can see in the Area and Iteration views.

image
Figure: The template is currently easily drag and drop, but you then need to edit the queries to point at the right Area and Iteration. This needs a tool.

I then created an “Areas” folder to hold all of the area specific queries. So, when you go to create a new TFS Sub-Project you just drag “_Area Template” while holding “Ctrl” and drop it onto “Areas”. There is a little setup here. That said I managed it in around 10 minutes which is not so bad, and I can imagine it being quite easy to build a tool to create these queries

image
Figure: These new queries can be configured in around 10 minutes, which includes setting up the Area and Iteration as well.

Version Control

What about your source code? Well, that is the easiest of the lot. Just create a sub folder for each of your projects/products.

 image
Figure: Creating sub folders in source control is easy as “Right click | Create new folder”.

<teamproject>\DEV\Main\

Becomes:

<teamproject>\<ProjectName>\DEV\Main\

Conclusion

I think it is up to each company to make a call on how you want to configure your Team Projects and it depends completely on how many projects/products you are going to have for each customer including yourself.

If we decide to utilise this route it will require some configuration to get our 170+ projects into this format, and I will probably be writing some tools to help.

Feedback

Now that I have explained this method, what do you think?

 

 

 

 

  • What other pros and cons can you see?
  • What do you think of this approach?
  • Will you be using it?
  • What tools would you like to support you?

 


Friday, March 05, 2010 #

I think that I have found one of the best articles on MVVM that I have ever read:

http://jmorrill.hjtcentral.com/Home/tabid/428/EntryId/432/MVVM-for-Tarded-Folks-Like-Me-or-MVVM-and-What-it-Means-to-Me.aspx

This article sums up what is in MVVM and what is outside of MVVM. Note, when I and most other people say MVVM, they really mean MVVM, Commanding, Dependency Injection + any other Patterns you need to create your application.

In WPF a lot of use is made of the Decorator and Behaviour pattern as well. The goal of all of this is to have pure separation of concerns. This is what every code behind file of every Control / Window / Page  should look like if you are engineering your WPF and Silverlight correctly:

C# – Ideal

  public partial class IdealView : UserControl
  {
      public IdealView()
      {
          InitializeComponent();
      }
  }

Figure: This is the ideal code behind for a Control / Window / Page when using MVVM.

C# – Compromise, but works

  public partial class IdealView : UserControl
  {
      public IdealView()
      {
          InitializeComponent();

          this.DataContext = new IdealViewModel();
      }
  }

Figure: This is a compromise, but the best you can do without Dependency Injection

VB.NET – Ideal

Partial Public Class ServerExplorerConnectView

End Class

Figure: This is the ideal code behind for a Control / Window / Page when using MVVM.

VB.NET – Compromise, but works

Partial Public Class ServerExplorerConnectView

    Private Sub ServerExplorerConnectView_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
        Me.DataContext = New ServerExplorerConnectViewModel
    End Sub

End Class

Figure: This is a compromise, but the best you can do without Dependency Injection

Technorati Tags: ,,,

Thursday, March 04, 2010 #

Now that I have the Build failing because of a genuine bug and not just because of a test framework failure, lets see if we can trace through to finding why the first test in our new application failed. Lets look at the build and see if we can see why there is a red cross on it.

First, lets open that build list. On Team Explorer Expand your Team Project Collection | Team Project and then Builds. Double click the offending build.

image
Figure: Opening the Build list is a key way to see what the current state of your software is.

 image
Figure: A test is failing, but we can now view the Test Results to find the problem

 

image  
Figure: You can quite clearly see that the test has failed with “The device is not ready”.

To me the “The Device is not ready” smacks of a System.IO exception, but it passed on my local computer, so why not on the build server?

Its a FaultException so it is most likely coming from the Service and not the client, so lets take a look at the client method that the test is calling:

bool IProfileService.SaveDefaultProjectFile(string strComputerName)
{            
    ProjectFile file = new ProjectFile()
    {
        ProjectFileName = strComputerName + "_" + System.DateTime.Now.ToString("yyyyMMddhhmmsss") + ".xml",
        ConnectionString = "persist security info=False; pooling=False; data source=(local); application name=SSW.SQLDeploy.vshost.exe; integrated security=SSPI; initial catalog=SSWSQLDeployNorthwindSample",
        DateCreated = System.DateTime.Now,
        DateUpdated = System.DateTime.Now,
        FolderPath = @"C:\Program Files\SSW SQL Deploy\SampleData\",
        IsComplete=false,
        Version = "1.3",
        NewDatabase = true,
        TimeOut = 5,
        TurnOnMSDE = false,
        Mode="AutomaticMode"
    };

    string strFolderPath = "D:\\"; //LocalSettings.ProjectFileBasePath;
    string strFileName = strFolderPath + file.ProjectFileName;

    try
    {
        using (FileStream fs = new FileStream(strFileName, FileMode.Create))
        {
            DataContractSerializer serializer = new DataContractSerializer(typeof(ProjectFile));
            using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter(fs))
            {
                serializer.WriteObject(writer, file);
            }
        }
    }
    catch (Exception ex)
    { 
        //TODO: Log the exception
        throw ex;
        return false;
    }

    return true;
}

Figure: You can see on lines 9 and 18 that there are calls being made to specific folders and disks.

What is wrong with this code? What assumptions mistakes could the developer have made to make this look OK:

  1. That every install would be to “C:\Program Files\SSW SQL Deploy”
  2. That every computer would have a “D:\\”
  3. That checking in code at 6pm because the had to go home was a good idea.

lets solve each of these problems:

  1. We are in a web service… lets store data within the web root. So we can call “Server.MapPath(“~/App_Data/SSW SQL Deploy\SampleData”) instead.
  2. Never reference an explicit path. If you need some storage for your application use IsolatedStorage.
  3. Shelve your code instead.

What else could have been done?

  1. Code review before check-in – The developer should have shelved their code and asked another dev to look at it.
  2. Use Defensive programming – Make sure that any code that has the possibility of failing has checks.

Any more options?

Let me know and I will add them.

What do we do?

The correct things to do is to add a Bug to the backlog, but as this is probably going to be fixed in sprint, I will add it directly to the sprint backlog.

  1. Right click on the failing test Select “Create Work Item | Bug”
    image
    Figure: Create an associated bug to add to the backlog.
  2. Set the values for the Bug making sure that it goes into the right sprint and Area. Make your steps to reproduce as explicit as possible, but “See test” is valid under these circumstances.
     image
    Figure: Add it to the correct Area and set the Iteration to the Area name or the Sprint if you think it will be fixed in Sprint and make sure you bring it up at the next Scrum Meeting.
    Note: make sure you leave the “Assigned To” field blank as in Scrum team members sign up for work, you do not give it to them. The developer who broke the test will most likely either sign up for the bug, or say that they are stuck and need help.
    Note: Visual Studio has taken care of associating the failing test with the Bug.
  3. Save…

Need Help?

www.SSW.com.au

SSW was the first company in the world outside of Microsoft to deploy Visual Studio 2010 Team Foundation Server to production, not once, but twice.

Team Foundation Server

Visual Studi2010 ALM SSW provides expert Visual Studio ALM guidance including installation, configuration and customisation through our four Microsoft Visual Studio ALM MVP’s in three countries; Australia, Beijing and the UK. They have experience deploying to small development shops all the way through to large blue chips.

Professional Scrum Developer Training

Professional Scrum Developer Training SSW has six Professional Scrum Developer Trainers who specialise in training your developers in implementing Scrum with Microsoft's Visual Studio ALM tools.


I recently had a fun time trying to debug a permission issue I ran into using TFS 2010’s TfsConfig.

Update 5th March 2010 – In its style of true excellence my company has added rant to its “Suggestions for Better TFS”.


<rant>

I was trying to run the TfsConfig tool and I kept getting the message:

“TF55038: You don't have sufficient privileges to run this tool. Contact your Team Foundation system administrator."

This message made me think that it was something to do with the Install permissions as it is always recommended to use a single account to do every install of TFS. I did not install the original TFS on our network and my account was not used to do the TFS2010 install. But I did do the upgrade from 2010 beta 2 to 2010 RC with my current account.

So I proceeded to do some checking:

  • Am I in the administrators group on the server?
    image
    Figure: Yes, I am in the administrators group on the server
  • Am I in the Administration Console users list?
    image
    Figure: Yes, I am in the Administration Console users list
  • Have I reapplied the permissions in the Administration Console users list ticking all the options?
    image
    Figure: Make sure you check all of the boxed if you want to have all the admin options

    image
    Figure: Yes, I have made sure that all my options are correct.

  • Am I in the Team Foundation administrators group?
    image
    Figure: Yes, I am in the Team Foundation Administrators group
  • Is my account explicitly SysAdmin on the Database server?
    image
    Figure: Yes, I do have explicit SysAdmin on the database

Can you guess what the problem was?

The command line window was not running as the administrator!

As with most other applications there should be an explicit error message that states:

"You are not currently running in administrator mode; please restart the command line with elevated privileges!"

This would have saved me 30 minutes, although I agree that I should change my name to Muppet and just be done with it.

</rant>

 


Guess what. About 20 minutes after I fixed the build, Allan broke it again!

clip_image002

Update: 4th March 2010 – After having huge problems getting this working I read Billy Wang’s post which showed me the light.


The problem here is that even though the test passes locally it will not during an Automated Build. When you send your tests to the build server it does not understand that you want to spin up the web site and run tests against that! When you run the test in Visual Studio it spins up the web site anyway, but would you expect your test to pass if you told the website not to spin up? Of course not. So, when you send the code to the build server you need to tell it what to spin up.

First, the best way to get the parameters you need is to right click on the method you want to test and select “Create Unit Test”. This will detect wither you are running in IIS or ASP.NET Development Server or None, and create the relevant tags.

clip_image002[4]

Figure: Right clicking on “SaveDefaultProjectFile” will produce a context menu with “Create Unit tests…” on it.

If you use this option it will AutoDetect most of the Attributes that are required.

/// <summary>
///A test for SSW.SQLDeploy.SilverlightUI.Web.Services.IProfileService.SaveDefaultProjectFile
///</summary>
// TODO: Ensure that the UrlToTest attribute specifies a URL to an ASP.NET page (for example,
// http://.../Default.aspx). This is necessary for the unit test to be executed on the web server,
// whether you are testing a page, web service, or a WCF service.
[TestMethod()]
[HostType("ASP.NET")]
[AspNetDevelopmentServerHost("D:\\Workspaces\\SSW\\SSW\\SqlDeploy\\DEV\\Main\\SSW.SQLDeploy.SilverlightUI.Web", "/")]
[UrlToTest("http://localhost:3100/")]
[DeploymentItem("SSW.SQLDeploy.SilverlightUI.Web.dll")]
public void SaveDefaultProjectFileTest()
{
    IProfileService target = new ProfileService(); // TODO: Initialize to an appropriate value
    string strComputerName = string.Empty; // TODO: Initialize to an appropriate value
    bool expected = false; // TODO: Initialize to an appropriate value
    bool actual;
    actual = target.SaveDefaultProjectFile(strComputerName);
    Assert.AreEqual(expected, actual);
    Assert.Inconclusive("Verify the correctness of this test method.");
}

Figure: Auto created code that shows the attributes required to run correctly in IIS or in this case ASP.NET Development Server

If you are a purist and don’t like creating unit tests like this then you just need to add the three attributes manually.

  • HostType – This attribute specified what host to use. Its an extensibility point, so you could write your own. Or you could just use “ASP.NET”.
  • UrlToTest – This specifies the start URL. For most tests it does not matter which page you call, as long as it is a valid page otherwise your test may not run on the server, but may pass anyway.
  • AspNetDevelopmentServerHost – This is a nasty one, it is only used if you are using ASP.NET Development Host and is unnecessary if you are using IIS. This sets the host settings and the first value MUST be the physical path to the root of your web application.

OK, so all that was rubbish and I could not get anything working using the MSDN documentation. Google provided very little help until I ran into Billy Wang’s post  and I heard that heavenly music that all developers hear when understanding dawns that what they have been doing up until now is just plain stupid.

I am sure that the above will work when I am doing Web Unit Tests, but there is a much easier way when doing web services.

You need to add the AspNetDevelopmentServer attribute to your code. This will tell MSTest to spin up an ASP.NET Development server to host the service. Specify the path to the web application you want to use.

[AspNetDevelopmentServer("WebApp1", "D:\\Workspaces\\SSW\\SSW\\SqlDeploy\\DEV\\Main\\SSW.SQLDeploy.SilverlightUI.Web")]
[DeploymentItem("SSW.SQLDeploy.SilverlightUI.Web.dll")]
[TestMethod]
public void ProfileService_Integration_SaveDefaultProjectFile_Returns_True()
{
    ProfileServiceClient target = new ProfileServiceClient();

    bool isTrue = target.SaveDefaultProjectFile("Mav");

    Assert.AreEqual(true, isTrue);
}

Figure: This AspNetDevelopmentServer will make sure that the specified web application is launched.

Now we can run the test and have it pass, but if the dynamically assigned ASP.NET Development server port changes what happens to the details in your app.config that was generated when creating a reference to the web service? Well, it would be wrong and the test would fail.

This is where Billy’s helper method comes in. Once you have created an instance of your service call, and it has loaded the config, but before you make any calls to it you need to go in and dynamically set the Endpoint address to the same address as your dynamically hosted Web Application.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Reflection;
using System.ServiceModel.Description;
using System.ServiceModel;

namespace SSW.SQLDeploy.Test
{
    class WcfWebServiceHelper
    {

        public static bool TryUrlRedirection(object client, TestContext context, string identifier)
        {
            bool result = true; 
            try { 
                PropertyInfo property = client.GetType().GetProperty("Endpoint");
                string webServer = context.Properties[string.Format("AspNetDevelopmentServer.{0}", identifier)].ToString(); 
                Uri webServerUri = new Uri(webServer); 
                ServiceEndpoint endpoint = (ServiceEndpoint)property.GetValue(client, null); 
                EndpointAddressBuilder builder = new EndpointAddressBuilder(endpoint.Address); 
                builder.Uri = new Uri(endpoint.Address.Uri.OriginalString.Replace(endpoint.Address.Uri.Authority, webServerUri.Authority)); 
                endpoint.Address = builder.ToEndpointAddress(); 
            } 
            catch (Exception e) { 
                context.WriteLine(e.Message); result = false; 
            }
            return result;
        }
    }
}

Figure: This fixes a problem with the URL in your web.config not being the same as the dynamically hosted ASP.NET Development server port.

We can now add a call to this method after we created the Proxy object and change the Endpoint for the Service to the correct one. This process is wrapped in an assert as if it fails there is no point in continuing.

[AspNetDevelopmentServer("WebApp1", D:\\Workspaces\\SSW\\SSW\\SqlDeploy\\DEV\\Main\\SSW.SQLDeploy.SilverlightUI.Web")]
[DeploymentItem("SSW.SQLDeploy.SilverlightUI.Web.dll")]
[TestMethod]
public void ProfileService_Integration_SaveDefaultProjectFile_Returns_True()
{
    ProfileServiceClient target = new ProfileServiceClient();
    Assert.IsTrue(WcfWebServiceHelper.TryUrlRedirection(target, TestContext, "WebApp1"));

    bool isTrue = target.SaveDefaultProjectFile("Mav");

    Assert.AreEqual(true, isTrue);
}

Figure: Editing the Endpoint from the app.config on the fly to match the dynamically hosted ASP.NET Development Server URL and port is now easy.

As you can imagine AspNetDevelopmentServer poses some problems of you have multiple developers. What are the chances of everyone using the same location to store the source? What about if you are using a build server, how do you tell MSTest where to look for the files?

To the rescue is a property called" “%PathToWebRoot%” which is always right on the build server. It will always point to your build drop folder for your solutions web sites. Which will be “\\tfs.ssw.com.au\BuildDrop\[BuildName]\Debug\_PrecompiledWeb\” or whatever your build drop location is. So lets change the code above to add this.

[AspNetDevelopmentServer("WebApp1", "%PathToWebRoot%\\SSW.SQLDeploy.SilverlightUI.Web")]
[DeploymentItem("SSW.SQLDeploy.SilverlightUI.Web.dll")]
[TestMethod]
public void ProfileService_Integration_SaveDefaultProjectFile_Returns_True()
{
    ProfileServiceClient target = new ProfileServiceClient();
    Assert.IsTrue(WcfWebServiceHelper.TryUrlRedirection(target, TestContext, "WebApp1"));

    bool isTrue = target.SaveDefaultProjectFile("Mav");

    Assert.AreEqual(true, isTrue);
}

Figure: Adding %PathToWebRoot% to the AspNetDevelopmentServer path makes it work everywhere.

Now we have another problem… this will ONLY run on the build server and will fail locally as %PathToWebRoot%’s default value is “C:\Users\[profile]\Documents\Visual Studio 2010\Projects”. Well this sucks… How do we get the test to run on any build server and any developer laptop.

Open “Tools | Options | Test Tools | Test Execution” in Visual Studio and you will see a field called “Web application root directory”. This is where you override that default above.

image
Figure: You can override the default website location for tests.

In my case I would put in “D:\Workspaces\SSW\SSW\SqlDeploy\DEV\Main” and all the developers working with this branch would put in the folder that they have mapped.

Can you see a problem?

What is I create a “$/SSW/SqlDeploy/DEV/34567” branch from Main and I want to run tests in there. Well… I would have to change the value above. This is not ideal, but as you can put your projects anywhere on a computer, it has to be done.

Conclusion

Although this looks convoluted and complicated there are real problems being solved here that mean that you have a test ANYWHERE solution. Any build server, any Developer workstation.

Resources:

http://billwg.blogspot.com/2009/06/testing-wcf-web-services.html

http://tough-to-find.blogspot.com/2008/04/testing-asmx-web-services-in-visual.html

http://msdn.microsoft.com/en-us/library/ms243399(VS.100).aspx

http://blogs.msdn.com/dscruggs/archive/2008/09/29/web-tests-unit-tests-the-asp-net-development-server-and-code-coverage.aspx

http://www.5z5.com/News/?543f8bc8b36b174f


Need Help?

www.SSW.com.au

SSW was the first company in the world outside of Microsoft to deploy Visual Studio 2010 Team Foundation Server to production, not once, but twice.

Team Foundation Server

Visual Studi2010 ALM SSW provides expert Visual Studio ALM guidance including installation, configuration and customisation through our four Microsoft Visual Studio ALM MVP’s in three countries; Australia, Beijing and the UK. They have experience deploying to small development shops all the way through to large blue chips.

Professional Scrum Developer Training

Professional Scrum Developer Training SSW has six Professional Scrum Developer Trainers who specialise in training your developers in implementing Scrum with Microsoft's Visual Studio ALM tools.


Wednesday, March 03, 2010 #

This is SSW’s first time using Team Build 2010 to automatically create a Silverlight application. In the past the guys have used Cruse Control, but we want to move to a pure TFS 2010 solution. When one of our developers (Allan) added a Silverlight 3 project to the Solution our build server spat it out.

clip_image001
Figure: Build SSW.SqlDeploy_20100303.8 failed when trying to build a Silverlight application.

Usually the person who broke the build should now be the one responsible for babysitting it until the next person breaks the build. In this case we had not agreed that as part of our project prep so I think I will need to wait until the retrospective at the end of our current, and first for this project, sprint.

Problem 1: First time for Silverlight compile on the Build Server.

Because Allan added the first Silverlight 3 application to the Solution the build server hiccupped as only the Silverlight 2 SDK was installed on it and it was a Silverlight 3 project. I have highlighted below where the problem was located.

 image
Figure: The Silverlight targets file was not found on the build server.

I downloaded and installed the Silverlight 3 SDK from Microsoft, and hoped all would be well.

But the build failed again…

clip_image003
Figure: SSW.SqlDeploy_20100303.10 failed still trying to find targets.

Problem 2: This was due to the web targets not being installed.

At this point I got fed up and copied the contents of my local directory “C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v10.0” to the same folder on the build server.

image
Figure: MSBuild could not find the web targets.

But the build failed again…

image
Figure: SSW.SqlDeploy_20100303.11 failed again trying to build Silverlight.

Problem 3: Can’t build Silverlight 3 projects with MSBuild 64-bit (the default)

There is a nasty bug in the Silverlight SDK that means that you can’t build using the MSBuild 64-bit process. But on a 64-bit OS, the 64-bit MSBuild is used by default… so how to change it.

image
Figure: Why is it saying that the SDK is not installed… oh, a bug.

This is an easy fix, but a hard to find solution. if you Open up your “build process definition” and expand the Advanced tree you will see that there is a MSBuild Platform option that is set to “Auto”, change this to “X86”.

clip_image005
Figure: You MUST set the MSBuild Platform to X86 to build a Silverlight project,

And the build failed again…

image
Figure: SSW.SqlDeploy_20100304.04 failed again trying to do code analysis.
Note: This was only run 20 or so minutes after the last build, but my build server happens to be in Australia :)

Problem 4: Can’t run Code Analysis on Build Server

Now I get Code Analysis errors…

image
Figure: Why would I be getting code analysis errors? Could it be that it is not installed?

To fix this one I just bit the bullet and installed Visual Studio 2010 onto the Build server, and…

clip_image007
Figure: Successful builds give me a warm fuzzy feeling…

Conclusion

The things that should be installed on the build server are:

  • Team Foundation Build Services 2010 or 2008 or 2005
  • Visual Studio 2010/2008/2005
  • Add-on’s for TFS or Visual Studio that may be required to execute the build.

The options you should set for any Build that has 32-bit dependencies that are causing a problem:

  • You MUST set the MSBuild Platform to X86 to build a project that can’t be built in 64-bit MSBuild.

 


Need Help?

www.SSW.com.au

SSW was the first company in the world outside of Microsoft to deploy Visual Studio 2010 Team Foundation Server to production, not once, but twice.

Team Foundation Server

Visual Studi2010 ALM SSW provides expert Visual Studio ALM guidance including installation, configuration and customisation through our four Microsoft Visual Studio ALM MVP’s in three countries; Australia, Beijing and the UK. They have experience deploying to small development shops all the way through to large blue chips.

Professional Scrum Developer Training

Professional Scrum Developer Training SSW has six Professional Scrum Developer Trainers who specialise in training your developers in implementing Scrum with Microsoft's Visual Studio ALM tools.


Wednesday, February 10, 2010 #

Visual Studio Team Foundation Server 2010 RC was released yesterday on MSDN. I am happy to report that today we successfully completed upgrading our production TFS 2010 Beta 2 server, to the new TFS 2010 RC. wow.

Updated: 2009-02-11 – Added link to Brian Harry’s post
Updated: 2009-02-12 – Adam Cogan was not clear that there were two problems with snapshoting running servers.


The upgrade was smooth, let me tell you the steps:

note: If you are upgrading from TFS 2008 you can follow our Rules to better TFS 2010 Migration

  1. Snapshot the hyper-v server 
    There are two reasons why you should never do this while the server is running:
    1. It’s Slow - Make sure you turn off your server before you take a snapshot. It took 15 minutes to get to 2% while the server was running, but turning it off had the whole operation completed in under 30 seconds. I think of this as very like the feature of Linux that let you recompile the kernel on the fly to avoid rebooting when adding drivers: Nice to have, but only if you have 10 hours to spare.
    2. It’s Dangerous - Brian Harry has an even better reason why you should never snapshot a running server.
  2. Uninstall Visual Studio Team Explorer 2010 Beta 2 
    You will need to uninstall all of the Visual Studio 2010 Beta 2 client bits that you have on the server. That's a no brainer, but you can remove them early to streamline your installation process
  3. Uninstall TFS 2010 Beta 2
  4. Install TFS 2010 RC
  5. Configure TFS 2010 RC
    Pick the Upgrade option and point it at your existing “tfs_Configuration” database to load all of the existing settings
  6. Test the server

All of our 52 developers are now up and running on TFS 2010 RC. Well…almost all. A couple of guys reported this problem even though they had previously connected to TFS 2010 Beta 2:

  1. If you get this error on the VS 2008 client after the upgrade, you should check whether you have KB74558 installed, if not you can download it manually or run diagnostics to ensure your entire system is up to date.
    clip_image002
    Figure: Error TF31001 or TF253022, but why is that link not clickable.

    clip_image001
    Figure:  Check that you have the update so you can connect to TFS 2010 via “Help | About Microsoft Visual Studio” 

I will be ironing out any other kinks tomorrow…

Next steps includes upgrading our build servers and moving all 52 developers over to Visual Studio 2010.

We were the first company on Beta 2 in production and I believe we are first on RC in production.

 


Need Help?

www.SSW.com.au

SSW was the first company in the world outside of Microsoft to deploy Visual Studio 2010 Team Foundation Server to production, not once, but twice.

Team Foundation Server

Visual Studi2010 ALM SSW provides expert Visual Studio ALM guidance including installation, configuration and customisation through our four Microsoft Visual Studio ALM MVP’s in three countries; Australia, Beijing and the UK. They have experience deploying to small development shops all the way through to large blue chips.

Professional Scrum Developer Training

Professional Scrum Developer Training SSW has six Professional Scrum Developer Trainers who specialise in training your developers in implementing Scrum with Microsoft's Visual Studio ALM tools.


Monday, February 08, 2010 #

<rant>

logo I am currently in Sydney Australia attending some training and meeting my boss for the first time. I was having a fantastic time until my wife phoned to let me know that Vodafone had called to say that the bill was over some limit and that they would be cutting my phone off if I did not contact them to confirm. Now, I had done the right thing and told them I would be abroad and where I was going, but I had forgotten to add my wife to the account. Fair enough…

So I called them and spoke to Customer Services to let them know that everything was OK, that I was happy to go over that limit, and add my wife to the account. They guy said that was fine…but next day my phone was not working.

I got my wife to phone them and they confirmed that they would reconnect the phone and that it would activate at 8am the next morning. It did not!

That was Vodafone's second chance!

She then called today and the operator denied that I had given her access to the account, and that it was even possible that she had spoken to customer services the day before. The operator then refused to put her through to a manager and cut her off…

That was their third chance!

If I was a pay as you go customer I would have left by now, but with a contract I am locked in for 18 months and my wife for 24 months :( being from Glasgow you can imagine the number of times I have had to edit this post for profanity and content.

I was a contract customer of Orange for 12 years prior to moving to Vodafone and I have to say that although I had a few small complaints with Orange their service and support is far superior.. I miss you Orange, I would never have left you if you had offered me a HTC HD2!

If you are thinking of going with Vodafone… stop, think and go somewhere else!

 

Technorati Tags: ,

</rant>


Saturday, January 09, 2010 #

We had a small problem today with a new site we were going live with. It was refusing to send emails in 90% of cases. Problems like these are always difficult to identify, but your first step is always to enable logging.

#Software: Microsoft Internet Information Services 7.0
#Version: 1.0
#Date: 2010-01-09 18:49:30
#Fields: c-ip cs-username s-sitename s-computername s-ip s-port cs-method cs-uri-query sc-win32-status cs-bytes cs-version cs(User-Agent) cs(Referer) 
127.0.0.1 MYHOST-MYSERVER SMTPSVC1 MYHOST-MYSERVER 127.0.0.1 0 EHLO +ServerName 0 18 SMTP - -
127.0.0.1 MYHOST-MYSERVER SMTPSVC1 MYHOST-MYSERVER 127.0.0.1 0 MAIL +FROM:enquiries@company.com 0 47 SMTP - -
127.0.0.1 MYHOST-MYSERVER SMTPSVC1 MYHOST-MYSERVER 127.0.0.1 0 RCPT +TO:&lt;martin@hinshelwood.com&gt; 0 32 SMTP - -
127.0.0.1 MYHOST-MYSERVER SMTPSVC1 MYHOST-MYSERVER 127.0.0.1 0 DATA &lt;MYHOST-MYSERVERVoMDrx0000015e@MYHOST-MYSERVER&gt; 0 2560 SMTP - -
216.146.33.4 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 220+mx1.mailhop.org+ESMTP+MailHop+by+DynDNS.com 0 0 SMTP - -
216.146.33.4 OutboundConnectionCommand SMTPSVC1 MYHOST-MYSERVER - 25 EHLO MYHOST-MYSERVER 0 0 SMTP - -
216.146.33.4 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 250-mx1.mailhop.org 0 0 SMTP - -
216.146.33.4 OutboundConnectionCommand SMTPSVC1 MYHOST-MYSERVER - 25 MAIL FROM:&lt;enquiries@company.com&gt;+SIZE=2884 0 0 SMTP - -
216.146.33.4 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 250+2.1.0+Ok 0 0 SMTP - -
216.146.33.4 OutboundConnectionCommand SMTPSVC1 MYHOST-MYSERVER - 25 RCPT TO:&lt;martin@hinshelwood.com&gt; 0 0 SMTP - -
216.146.33.4 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 504+5.5.2+&lt;MYHOST-MYSERVER&gt;:+Helo+command+rejected:+need+fully-qualified+hostname 0 0 SMTP - -
216.146.33.4 OutboundConnectionCommand SMTPSVC1 MYHOST-MYSERVER - 25 RSET - 0 0 SMTP - -
216.146.33.3 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 220+mx1.mailhop.org+ESMTP+MailHop+by+DynDNS.com 0 0 SMTP - -
216.146.33.3 OutboundConnectionCommand SMTPSVC1 MYHOST-MYSERVER - 25 EHLO MYHOST-MYSERVER 0 0 SMTP - -
216.146.33.3 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 250-mx1.mailhop.org 0 0 SMTP - -
216.146.33.3 OutboundConnectionCommand SMTPSVC1 MYHOST-MYSERVER - 25 MAIL FROM:&lt;enquiries@company.com&gt;+SIZE=2884 0 0 SMTP - -
216.146.33.3 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 250+2.1.0+Ok 0 0 SMTP - -
216.146.33.3 OutboundConnectionCommand SMTPSVC1 MYHOST-MYSERVER - 25 RSET - 0 0 SMTP - -
216.146.33.3 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 250+2.0.0+Ok 0 0 SMTP - -
216.146.33.3 OutboundConnectionCommand SMTPSVC1 MYHOST-MYSERVER - 25 QUIT - 0 0 SMTP - -
216.146.33.3 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 221+2.0.0+Bye 0 0 SMTP - -


Figure: The log shows the source of the problem.
 

“5.5.2 rejected: need fully qualified hostname” tends to be destination server specific and relates to the server name that the mail is sent from which is different from the email from name. Most mail servers will reject mail from a name that they cannot lookup in DNS as an anti-spam measure.

To fix:

  1. I opened “Internet Information Services (IIS) 6.0 Manager” on the server.
    clip_image001
  2. Expanded and then right click on “[SMTP Virtual Server #1]” and select “Properties
    image 
  3. Select the “Delivery” Tab and then “Advanced
    clip_image003
  4. Enter “company.com” in the “fully-qualified domain name” field.
    image 
  5. Click “ok” and then “ok” to save the changes

You should now be able to send emails from your site without any problems.

Technorati Tags: ,,,

Monday, January 04, 2010 #

From time to time, your website structure may change. When this happens, you do not want to have to start from scratch with your Google rankings, so you need to map all of your Old URLs to new ones.

This may seem like a trivial thing, but it is essential to keep your current rankings, that you worked hard for, intact.

In our scenario the old site used a query string with the product ID in it, and the new site uses nice friendly names.

Old: http://northwind.com/Product/ProductInfo.aspx?id=3456

New: http://northwind.com/CoolLightsaberWithRealAction.aspx

Updated #1 January 5th, 2010: - As suggested by Adam Cogan, I changed:

  • Change Figures to SSW standard with “Good example, ….”, “OK example, ….” And “Bad example, ….“
  • Prefix main headings with “Option #x” to make them stand out
  • Prefix process steps with “Step #x” to differentiate them.
  • Remove tiny URL’s so the reader knows where they are going
  • Spell check (i.e. run through Word)
  • Link to rules for better regex
  • Change “outtakes” to “TODO:”’s
  • End on an impact line (“In conclusion I …..”)
  • Change Title to  “Solution - SEO permanent redirects for old URL’s?”

Updated #2 January 6th, 2010: –As suggested by John Liu, I changed the SQL call to be completely wrapped in a “try catch” statement and to close the connection in the “Finally” area. Dam, I thought no one at SSW could read VB.

Updated #3 January 7th, 2010: - As suggested by Peter Gfader, I changed the source to use a parameterised SQL statement instead of a Stored Procedure. He pointed out that “Stored procedures are bad, m'kay?

Updated #4 January 8th, 2010: – Updated to reflect latest code changes to increase flexibility of the rule.


Option #1 - You can do it in product.aspx

// …
// Lookup database here and find the friendly name for the product with the ID 3456
// …
Response.Status = "301 Moved Permanently"
Response.StatusCode = 301;   
Response.AddHeader("Location","/CoolLightsaberWithRealAction.aspx");
Response.End();

Figure: Bad example, Write it right into the old page.


Why is this not a good approach?

  • The old page may not exist, you may be building a whole new version of the site
  • It is slow. You have to wait for the page to load, which probably means your master page, and all the code which goes with that.
  • It leaves old pages dotted about your site that you do not really want.

Option #2 - You can do it in the global.asax

protected void Application_BeginRequest(object sender, EventArgs e)
{
    
    if (Request.FilePath.Contains("/product.aspx?id=") 
    {
        // ...
        // Lookup the ID in the database to get the new friendly name
        // ...
        Response.Status="301 Moved Permanently"
        Response.StatusCode=301;
        Response.Redirect ("/CoolLightsaberWithRealAction.aspx", true);
    }
}

Figure: Bad example, ASP.NET 2.0 solution in the global.asax file for redirects


protected void Application_BeginRequest(object sender, EventArgs e)
{
    
    if (Request.FilePath.Contains("/product.aspx?id=") 
    {
        // ...
        // Lookup the ID in the database to get the new friendly name
        // ...
        Response.RedirectPermanent("/CoolLightsaberWithRealAction.aspx", true);
    }
}

Figure: Bad example, ASP.NET 4.0 solution in the global.asax file for redirects, less code.


Using the global.asax has its draw backs.

  • To change it you must make a code change to your site and re-deploy
  • If you have multiple redirects it is going to get ugly fast.

Option #3 - You can do it with the IIS7 URL Rewrite Module

Using the IIS7 URL Rewrite Module which can be installed using the Microsoft Web Platform Installer is the best option, but unfortunately it does not currently support looking up a database.

If you have identifiable patterns in the rewrites that you want to perform then this is fantastic. So if you have all of the information that you need in the URL to do the rewrite, then you can stop reading and go an install it.

With the IIS7 URL Rewrite Module you can

  • Rewrite and redirect URLs
  • Handles requests before ASP.NET is aware of (good performance)
  • Solves both problems: redirecting broken pages and creating nice URLs
  • Various rule actions. Instead of rewriting a URL, a rule may perform other actions, such as issue an HTTP redirect, abort the request, or send a custom status code to HTTP client.
  • Nice graphical rule editor
  • Regex pattern matching for requests and rewrites
  • URL rewrite module v2 adds support for outbound response rewriting
  • Fix up the content of any HTTP response by using regular expression pattern matching (e.g. modify links in outgoing response)

As it turns out, we found out yesterday that the next version of the IIS7 URL Rewriting Module IS going to support loading from a database! Wither that is just loading the rules, or you can load some of the data you need has yet to be seen. But as we can’t get even a beta for a couple of weeks, and our release date is in that region we could not wait.

Option #4 - You can do it with UrlRewritingNet.UrlRewriter

Using the UrlRewritingNet.UrlRewriter component you can do pretty much everything that the IIS7 Rewrite Module does, but it does not have a nice UI to interact with. The best part of UrlRewritingNet.UrlRewriter is that its rules engine is extensible, so we can add a new rule type to load from a database.

The first thing you do with any new toolkit is read the documentation, or at least have it open and pretend to read it while you tinker.

Step #1 - Add UrlRewritingNet.UrlRewriter to our site

To add UrlRewritingNet.UrlRewriter to our site you need to add UrlRewritingNet.UrlRewriter.dll (you can download this from their site) to the Bin folder and make a couple of modifications to the web.config. I have opted to add the UrlRewritingNet.UrlRewriter section of the config to a separate file as this makes it more maintainable.

<?xml version="1.0"?>
<urlrewritingnet xmlns="http://www.urlrewriting.net/schemas/config/2006/07">
  <providers>
    <!-- providers go here -->
  </providers>
  <rewrites>
      <!-- rules go here -->
  </rewrites>
</urlrewritingnet>

Figure: Boilerplate URLRewriting config.


Create a new blank file called "urlrewriting.config" and insert the code above. As you can see you can add numerous providers and rules. Lookup the documentation for the built in rules model that uses the same method we will be using to capture URL's, but has a regular expression based replace implementation that lets you reform any URL into any other URL, provided all the values you need are either static, or included in the incoming URL.

<configSections>
  ...
  <section name="urlrewritingnet"
      restartOnExternalChanges="true"
      requirePermission="false"
      type="UrlRewritingNet.Configuration.UrlRewriteSection, UrlRewritingNet.UrlRewriter"       />
</configSections>  

Figure: ASP.NET Section definition for URLRewriting.


In your "web.config" add this section.

<urlrewritingnet configSource="UrlRewrite.config" />

Figure: You can use an external file or inline.


After the sections definition, but NOT inside any other section, add the section implementation, but use the "configSource" tag to map it to the "urlrewriting.config" file you created previously. You could also just add the contents of "urlrewriting.config" under "urlrewritingnet" element and remove the need for the additional file, but I think this is neater.

<system.web>
  <httpModules>
    <add name="UrlRewriteModule"
      type="UrlRewritingNet.Web.UrlRewriteModule, UrlRewritingNet.UrlRewriter" />
  </httpModules>
</system.web>

Figure: HttpModules make it all work in IIS6.


We need IIS to know that it needs to do some processing, but there are some key differences between IIS6 and IIS7, to make sure that both load your rewrite correctly, especially if you still have developers on Windows XP, you will need to add both of them. Add this one to the "HttpModules" element, before any other rewriting modules, it tells IIS6 that it needs to load the module.

<system.webServer>
  <modules>
    <add name="UrlRewriteModule"
      type="UrlRewritingNet.Web.UrlRewriteModule, UrlRewritingNet.UrlRewriter" />
  </modules>
</system.webServer>

Figure: Modules make it all work in IIS7.

II7 does things a little differently, so add the above to the "modules" element of "system.webServer". This does exactly the same thing, but slots it into the IIS7 pipeline.

You should now be able to add rules as specified in the documentation and have them run successfully, provided you have your regular expression  is correct :), but for this process we need to write our custom rule.

Step #2 - Creating a blank custom rule

For some reason I have not yet fathomed, you need to create a “Provider” as well. It just has boilerplate code, but I would assume that there are circumstances when it would be useful to have some code in there.

Imports UrlRewritingNet.Configuration.Provider

Public Class SqlUrlRewritingProvider
    Inherits UrlRewritingProvider

    Public Overrides Function CreateRewriteRule() As UrlRewritingNet.Web.RewriteRule
        Return New SqlRewriteRule
    End Function

End Class

Figure: Simple code for the provider.

All you need to do in the Provider is override the “CreateRewriteRule” and pass back an instance of your custom rule.

Imports UrlRewritingNet.Web
Imports UrlRewritingNet.Configuration
Imports System.Configuration

Public Class SqlRewriteRule
    Inherits RewriteRule

    Public Overrides Sub Initialize(ByVal rewriteSettings As RewriteSettings)
        MyBase.Initialize(rewriteSettings)
    End Sub

    Public Overrides Function IsRewrite(ByVal requestUrl As String) As Boolean
        Return false
    End Function

    Public Overrides Function RewriteUrl(ByVal url As String) As String
        Return url
    End Function

End Class

Figure: Boilerplate Rule.


This is a skeleton of a new rule. It does nothing now, and in fact will not run as long as the “IsRewrite” function returns false.

The “Initialize” method passes any setting that are set on the rule entry in the config file. As we want to create a dynamic and reusable rule, we will be using a lot of settings. The settings are written as Attributes in the XML, but are in effect name value pairs.

The “IsRewrite” will determine wither we want to run the logic behind the rule. I would not advice any performance intensive calls here (like calling the database), so you should find a quick and easy way of determining if we want to proceed to rewrite the URL. The best way of doing this will be via a regular expression.

“RewiteUrl” provides the actual logic to do the rewrite. We will be calling the database here so this is more intensive work.

Step #3 - Capture the URL you want to rewrite

Let’s first consider the capturing of the URL so we can do the IsRewrite. To provide our regular expression we will need to options, the first being our pattern, the second being the Regular expression options. We add the options so we can have both Case sensitive and insensitive settings. The standard field name for regular expressions that match is “VirtualUrl” we will just call the other “RegexOptions”.

Imports UrlRewritingNet.Web
Imports UrlRewritingNet.Configuration
Imports System.Data.SqlClient
Imports System.Text.RegularExpressions
Imports System.Configuration

Public Class SqlRewriteRule
    Inherits RewriteRule

    Private m_regexOptions As Text.RegularExpressions.RegexOptions
    Private m_virtualUrl As String = String.Empty

    Public Overrides Sub Initialize(ByVal rewriteSettings As RewriteSettings)
        Me.m_regexOptions = rewriteSettings.GetEnumAttribute(Of RegexOptions)("regexOptions", RegexOptions.None)
        Me.m_virtualUrl = rewriteSettings.GetAttribute("virtualUrl", "")
        MyBase.Initialize(rewriteSettings)
    End Sub

    Public Overrides Function IsRewrite(ByVal requestUrl As String) As Boolean
        Return true
    End Function

    Public Overrides Function RewriteUrl(ByVal url As String) As String
        Return url
    End Function

   
End Class

Figure: Retrieving values from the config is easy.


In order to capture these values we just add two fields to our class, and parse out the data from “rewriteSettings” for these two fields in the Initialize method.

Imports UrlRewritingNet.Web
Imports UrlRewritingNet.Configuration
Imports System.Text.RegularExpressions
Imports System.Configuration

Public Class ProductKeyRewriteRule
    Inherits RewriteRule

    Private m_regex As Text.RegularExpressions.Regex
    Private m_regexOptions As Text.RegularExpressions.RegexOptions
    Private m_virtualUrl As String = String.Empty

    ' Methods
    Private Sub CreateRegEx()
        Dim helper As New UrlHelper
        If MyBase.IgnoreCase Then
            Me.m_regex = New Regex(helper.HandleRootOperator(Me.m_virtualUrl), ((RegexOptions.Compiled Or RegexOptions.IgnoreCase) Or Me.m_regexOptions))
        Else
            Me.m_regex = New Regex(helper.HandleRootOperator(Me.m_virtualUrl), (RegexOptions.Compiled Or Me.m_regexOptions))
        End If
    End Sub

    Public Overrides Sub Initialize(ByVal rewriteSettings As RewriteSettings)
        Me.m_regexOptions = rewriteSettings.GetEnumAttribute(Of RegexOptions)("regexOptions", RegexOptions.None)
        Me.m_virtualUrl = rewriteSettings.GetAttribute("virtualUrl", "")
        CreateRegEx
        MyBase.Initialize(rewriteSettings)
    End Sub

    Public Overrides Function IsRewrite(ByVal requestUrl As String) As Boolean
        Return Me.m_regex.IsMatch(requestUrl)
    End Function

    Public Overrides Function RewriteUrl(ByVal url As String) As String
        Return url
    End Function

   
End Class

Figure: Creating an instance of a regular expression and using that is always faster than creating one each time.


We now have all of the information we need to create a regular expression and call "IsMatch" in the "IsRewrite" method. Therefore, we add another field for the regular expression and add a “CreateRegEx” method to create our regular expression using the built in “Ignorecase” option as well as our “RegexOptions” value. This creates a single compiled copy of our regular expression so it will operate as quickly as possible. Remember that this code will now be called for EVERY incoming URL request.

Step #4 - Rewrite the URL with data from the database

Now that we have captured the URL, we need to rewrite it. in order to do this we will need some extra fields, and this is were things get a little complicated because we want to be generic. We will need:

  • a connection string so we know where to load the data from
  • a SQL Statement
  • some input parameters for our SQL
  • some output data
  • a destination URL to inject our output into
  • a place to redirect users to if all else fails

The connection string is easy, or is it.

' Test for connectionString and throw exception if not available
m_ConnectionString = rewriteSettings.GetAttribute("connectionString", String.Empty)
If m_ConnectionString = String.Empty Then
    Throw New NotSupportedException(String.Format("You must specify a connectionString attribute for the DataRewriteRule {0}", rewriteSettings.Name))
End If
' Check to see if this is a named connection string
Dim NamedConnectionString As ConnectionStringSettings = ConfigurationManager.ConnectionStrings(m_ConnectionString)
If Not NamedConnectionString Is Nothing Then
    m_ConnectionString = NamedConnectionString.ConnectionString
End If

Figure: Make sure that you check wither values are correct.


There are two ways for a connection string to be stored in ASP.NET, inline and shared. We don’t want to be fixed to a specific type, so we need to assume shared and if we can’t find a shared string, assume that the string provided in the connection string and not a key for the shared string.

The stored procedure is just a string, but the input parameters, now that is a quandary. Where can we get them from and now can we configure them. Although it would probably be best if we could have sub elements to the rule definition in the “web.config” we can’t, so all we have is a set of name value pairs.

^.*/Product/ProductInfo\.aspx\?id=(?'ProductId'\d+)

Figure: Follow the rule: Do you test your regular expressions?



The solution I went for was to use Named groups in the regular expression. The only input parameter with this expression would be “@ProductId” and should be populated by the data in the capture group for the regular expression.

' Get all the named groups from the regular expression and use them as the stored procedure parameters.
Dim groupNames = From groupName In m_regex.GetGroupNames Where Not groupName = String.Empty And Not IsNumeric(groupName)
' Iterate through the named groups
For Each groupName As String In groupNames
   ' Add the name and value to the saved replacements
   UrlReplacements.Add(groupName, match.Groups(groupName).ToString)
   ' Add the name and value as input prameters to the stored procedure
   cmd.Parameters.AddWithValue("@" & groupName, match.Groups(groupName).ToString)
Next

Figure: Retrieving the named groups is easier than you think, but remember that it also contains the unnamed groups as a number.


So for each of the group names found in the regular expression I will be adding a SqlParameter to the SqlCommand object with the value that is returned. Again, a better solution would be to have meta data along with this that would identify the input parameters as well as data types and where to get them from, but alas it is not possible in this context.

All this allows you to call a parameterised SQL statement and get some data back that you can use in the “RewriteUrl” method. I created a “GetUrlReplacements” method to encapsulate this logic.

Private Function GetUrlReplacements(ByVal match As Match) As Dictionary(Of String, String)
    Dim UrlReplacements As New Dictionary(Of String, String)
    Dim paramString As String = String.Empty
    ' Call database
    Using conn As New SqlConnection(m_ConnectionString)
        Try
            conn.Open()
            Dim cmd As New SqlCommand(m_parameterisedSql, conn)
            cmd.CommandType = CommandType.Text
            ' Get all the named groups from the regular expression and use them as the stored procedure parameters.
            Dim groupNames = From groupName In m_regex.GetGroupNames Where Not groupName = String.Empty And Not IsNumeric(groupName)
            ' Iterate through the named groups
            For Each groupName As String In groupNames
                ' Add the name and value as input prameters to the stored procedure
                cmd.Parameters.AddWithValue("@" & groupName, match.Groups(groupName).ToString)
                paramString = paramString & "[@" & groupName & "=" & match.Groups(groupName).ToString & "]"
                If UrlReplacements.ContainsKey(groupName) Then
                    UrlReplacements.Add(groupName, match.Groups(groupName).ToString)
                Else
                    UrlReplacements(groupName) = match.Groups(groupName).ToString
                End If
            Next
            ' Defigne the data capture method
            Dim sqlReader As SqlClient.SqlDataReader
            ' Execute the SQL
            sqlReader = cmd.ExecuteReader()
            If sqlReader.HasRows Then
                Dim isDone As Boolean = False
                Do While sqlReader.Read()
                    If isDone Then
                        ' If more than one record is returned, exit and record
                        My.Application.Log.WriteEntry(String.Format("Too many results from execution of '{0}' using parameters '{1}' on the connection '{2}'. Make sure your query only returns a single record.", m_parameterisedSql, paramString, m_ConnectionString), TraceEventType.Error, 19786)
                        Exit Do
                    End If
                    ' Add a sql output parameter for each outputParam (note: Must be NVarChar(255))
                    For i As Integer = 0 To sqlReader.FieldCount - 1
                        If UrlReplacements.ContainsKey(sqlReader.GetName(i)) Then
                            UrlReplacements.Add(sqlReader.GetName(i), sqlReader.GetValue(i).ToString)
                        Else
                            UrlReplacements(sqlReader.GetName(i)) = sqlReader.GetValue(i).ToString
                        End If
                    Next
                    isDone = True
                Loop
                sqlReader.Close()
            Else
                My.Application.Log.WriteEntry(String.Format("No results from execution of '{0}' using parameters '{1}' on the connection '{2}'", m_parameterisedSql, paramString, m_ConnectionString), TraceEventType.Error, 19784)
                UrlReplacements.Clear()
                UrlReplacements.Add("results", "None")
            End If
        Catch ex As System.Data.SqlClient.SqlException
            My.Application.Log.WriteException(ex, TraceEventType.Error, String.Format("Unable to execute '{0}' using parameters '{1}' on the connection '{2}'", m_parameterisedSql, paramString, m_ConnectionString), 19783)
            UrlReplacements.Clear()
            UrlReplacements.Add("ex", "SqlException")
        Catch ex As Exception
            My.Application.Log.WriteException(ex, TraceEventType.Error, String.Format("Unable to connect using the connection '{0}'", m_ConnectionString), 19782)
            UrlReplacements.Clear()
            UrlReplacements.Add("ex", ex.GetType.ToString)
        End Try
    End Using
    Return UrlReplacements
End Function

Figure: Always encapsulate your more complicated logic, especially database calls.


The SQL is called and the first, and only the first, returned record is parsed into a name value collection allowing for multiple values to be returned.

Now that we have the relevant data, we can rewrite the URL.

Public Overrides Function RewriteUrl(ByVal url As String) As String
    ' Get the url replacement values
    Dim UrlReplacements As Dictionary(Of String, String) = GetUrlReplacements(Me.m_regex.Match(url))
    ' Take a copy of the target url
    Dim newUrl As String = m_destinationUrl
    ' Replace any valid values with the new value
    For Each key As String In UrlReplacements.Keys
        newUrl = newUrl.Replace("{" & key & "}", UrlReplacements(key))
    Next
    ' Test to see is any failed by looking for any left over '{'
    If newUrl.Contains("{") Then
        ' If there are left over bits, then only do a Tempory redirect to the failed URL
        Me.RedirectMode = RedirectModeOption.Temporary
        My.Application.Log.WriteEntry(String.Format("Unable to locate a product url replacement for {0}", url), TraceEventType.Error, 19781)
        Return (String.Format(m_RedirectToOnFail, Me.Name, "NotFound", UrlReplacementsToQueryString(UrlReplacements)))
    End If
    ' Sucess, so do a perminant redirect to the new url.
    My.Application.Log.WriteEntry(String.Format("Redirecting {0} to {1}", url, newUrl), TraceEventType.Information, 19780)
    Me.RedirectMode = RedirectModeOption.Permanent
    Return newUrl.Replace("^", "")
End Function

Figure: Make sure that there is a backup plan for your rewrites.


As you can see all we do once we have the replacement values is replace the keys from the “DestinationUrl” value with the new values. One additional test is done to check that we have not miss-configured and left some values out, so check to see if there are any “{“ left and redirect to the  “redirectOnFailed” location if we did. This will be caught if either we did not get any data back, or we just messed up the configuration.

Lets setup the rule in the config.

<?xml version="1.0"?>
<urlrewritingnet xmlns="http://www.urlrewriting.net/schemas/config/2006/07">
  <providers>
    <add name="SqlUrlRewritingProvider" type="SSW.UrlRewriting.SqlUrlRewritingProvider, SSW.UrlRewriting"/>
  </providers>
  <rewrites>
    <add name="Rule2"
        provider="SqlUrlRewritingProvider"
        connectionString="MyConnectionString"
        virtualUrl="^.*/Product/ProductInfo\.aspx\?id=(?'ProductId'\d+)"
        parameterisedSql="SELECT dbo.CatalogEntry.Code as ProductId, dbo.CatalogItemSeo.Uri as ProductKey FROM  dbo.CatalogEntry INNER JOIN dbo.CatalogItemSeo ON dbo.CatalogEntry.CatalogEntryId = dbo.CatalogItemSeo.CatalogEntryId WHERE Code = @ProductId"
        DestinationUrl="^~/{ProductKey}"
        rewriteUrlParameter="IncludeQueryStringForRewrite"
        redirectToOnFail="~/default.aspx?rewrite=productNotFound"
        redirectMode="Permanent"
        redirect="Application"
        rewrite="Application"
        ignoreCase="true" />
  </rewrites>
</urlrewritingnet>

Figure: You can configure as many rules as you like.


The final config entry for the rule looks complicated, but it should all make sense to you now that all the logic has been explained. There are some additional propertied here that are part of the Rewriting engine, but you will find them all in the documentation.

In conclusion, hopefully the IIS7 module will support a more elegant solution in its next iteration, and you can always just hard code an HttpModule. This however is the beginnings of a more dynamic solution that can be used over and over again, even in the one site.

For those of you that can’t be bothered to piece this all together, here is the full rule source, but Don’t forget to skip to the bottom for the TODO.

Imports UrlRewritingNet.Web
Imports UrlRewritingNet.Configuration
Imports System.Data.SqlClient
Imports System.Text.RegularExpressions
Imports System.Configuration

Public Class SqlRewriteRule
    Inherits RewriteRule

    Private m_ConnectionString As String
    Private m_parameterisedSql As String
    Private m_destinationUrl As String = String.Empty
    Private m_regex As Text.RegularExpressions.Regex
    Private m_regexOptions As Text.RegularExpressions.RegexOptions
    Private m_virtualUrl As String = String.Empty
    Private m_RedirectToOnFail As String

    ' Methods
    Private Sub CreateRegEx()
        Dim helper As New UrlHelper
        If MyBase.IgnoreCase Then
            Me.m_regex = New Regex(helper.HandleRootOperator(Me.m_virtualUrl), ((RegexOptions.Compiled Or RegexOptions.IgnoreCase) Or Me.m_regexOptions))
        Else
            Me.m_regex = New Regex(helper.HandleRootOperator(Me.m_virtualUrl), (RegexOptions.Compiled Or Me.m_regexOptions))
        End If
    End Sub

    Public Overrides Sub Initialize(ByVal rewriteSettings As RewriteSettings)
        Me.m_regexOptions = rewriteSettings.GetEnumAttribute(Of RegexOptions)("regexOptions", RegexOptions.None)
        Me.m_virtualUrl = rewriteSettings.GetAttribute("virtualUrl", "")
        Me.m_destinationUrl = rewriteSettings.GetAttribute("destinationUrl", "")
        Me.CreateRegEx()
        ' Test for connectionString and throw exception if not available
        m_ConnectionString = rewriteSettings.GetAttribute("connectionString", String.Empty)
        If m_ConnectionString = String.Empty Then
            Throw New NotSupportedException(String.Format("You must specify a connectionString attribute for the DataRewriteRule {0}", rewriteSettings.Name))
        End If
        ' Check to see if this is a named connection string
        Dim NamedConnectionString As ConnectionStringSettings = ConfigurationManager.ConnectionStrings(m_ConnectionString)
        If Not NamedConnectionString Is Nothing Then
            m_ConnectionString = NamedConnectionString.ConnectionString
        End If
        ' Test for storedProcedure and throw exception if not available
        m_parameterisedSql = rewriteSettings.GetAttribute("parameterisedSql", String.Empty)
        If m_parameterisedSql = String.Empty Then
            Throw New NotSupportedException(String.Format("You must specify a parameterisedSql attribute for the DataRewriteRule {0}", rewriteSettings.Name))
        End If

        ' Test for redirectToOnFail and throw exception if not available
        m_RedirectToOnFail = rewriteSettings.GetAttribute("redirectToOnFail", String.Empty)
        If m_RedirectToOnFail = String.Empty Then
            Throw New NotSupportedException(String.Format("You must specify a redirectToOnFail attribute for the DataRewriteRule {0}", rewriteSettings.Name))
        End If
        MyBase.Initialize(rewriteSettings)
    End Sub

    Public Overrides Function IsRewrite(ByVal requestUrl As String) As Boolean
        Return Me.m_regex.IsMatch(requestUrl)
    End Function

    Public Overrides Function RewriteUrl(ByVal url As String) As String
        ' Get the url replacement values
        Dim UrlReplacements As Dictionary(Of String, String) = GetUrlReplacements(Me.m_regex.Match(url))
        ' Take a copy of the target url
        Dim newUrl As String = m_destinationUrl
        ' Replace any valid values with the new value
        For Each key As String In UrlReplacements.Keys
            newUrl = newUrl.Replace("{" & key & "}", UrlReplacements(key))
        Next
        ' Test to see is any failed by looking for any left over '{'
        If newUrl.Contains("{") Then
            ' If there are left over bits, then only do a Tempory redirect to the failed URL
            Me.RedirectMode = RedirectModeOption.Temporary
            My.Application.Log.WriteEntry(String.Format("Unable to locate a product url replacement for {0}", url), TraceEventType.Error, 19781)
            Return (String.Format(m_RedirectToOnFail, Me.Name, "NotFound", UrlReplacementsToQueryString(UrlReplacements)))
        End If
        ' Sucess, so do a perminant redirect to the new url.
        My.Application.Log.WriteEntry(String.Format("Redirecting {0} to {1}", url, newUrl), TraceEventType.Information, 19780)
        Me.RedirectMode = RedirectModeOption.Permanent
        Return newUrl.Replace("^", "")
    End Function

    Private Function GetUrlReplacements(ByVal match As Match) As Dictionary(Of String, String)
        Dim UrlReplacements As New Dictionary(Of String, String)
        Dim paramString As String = String.Empty
        ' Call database
        Using conn As New SqlConnection(m_ConnectionString)
            Try
                conn.Open()
                Dim cmd As New SqlCommand(m_parameterisedSql, conn)
                cmd.CommandType = CommandType.Text
                ' Get all the named groups from the regular expression and use them as the stored procedure parameters.
                Dim groupNames = From groupName In m_regex.GetGroupNames Where Not groupName = String.Empty And Not IsNumeric(groupName)
                ' Iterate through the named groups
                For Each groupName As String In groupNames
                    ' Add the name and value as input prameters to the stored procedure
                    cmd.Parameters.AddWithValue("@" & groupName, match.Groups(groupName).ToString)
                    paramString = paramString & "[@" & groupName & "=" & match.Groups(groupName).ToString & "]"
                    If UrlReplacements.ContainsKey(groupName) Then
                        UrlReplacements.Add(groupName, match.Groups(groupName).ToString)
                    Else
                        UrlReplacements(groupName) = match.Groups(groupName).ToString
                    End If
                Next
                ' Defigne the data capture method
                Dim sqlReader As SqlClient.SqlDataReader
                ' Execute the SQL
                sqlReader = cmd.ExecuteReader()
                If sqlReader.HasRows Then
                    Dim isDone As Boolean = False
                    Do While sqlReader.Read()
                        If isDone Then
                            ' If more than one record is returned, exit and record
                            My.Application.Log.WriteEntry(String.Format("Too many results from execution of '{0}' using parameters '{1}' on the connection '{2}'. Make sure your query only returns a single record.", m_parameterisedSql, paramString, m_ConnectionString), TraceEventType.Error, 19786)
                            Exit Do
                        End If
                        ' Add a sql output parameter for each outputParam (note: Must be NVarChar(255))
                        For i As Integer = 0 To sqlReader.FieldCount - 1
                            If UrlReplacements.ContainsKey(sqlReader.GetName(i)) Then
                                UrlReplacements.Add(sqlReader.GetName(i), sqlReader.GetValue(i).ToString)
                            Else
                                UrlReplacements(sqlReader.GetName(i)) = sqlReader.GetValue(i).ToString
                            End If
                        Next
                        isDone = True
                    Loop
                    sqlReader.Close()
                Else
                    My.Application.Log.WriteEntry(String.Format("No results from execution of '{0}' using parameters '{1}' on the connection '{2}'", m_parameterisedSql, paramString, m_ConnectionString), TraceEventType.Error, 19784)
                    UrlReplacements.Clear()
                    UrlReplacements.Add("results", "None")
                End If
            Catch ex As System.Data.SqlClient.SqlException
                My.Application.Log.WriteException(ex, TraceEventType.Error, String.Format("Unable to execute '{0}' using parameters '{1}' on the connection '{2}'", m_parameterisedSql, paramString, m_ConnectionString), 19783)
                UrlReplacements.Clear()
                UrlReplacements.Add("ex", "SqlException")
            Catch ex As Exception
                My.Application.Log.WriteException(ex, TraceEventType.Error, String.Format("Unable to connect using the connection '{0}'", m_ConnectionString), 19782)
                UrlReplacements.Clear()
                UrlReplacements.Add("ex", ex.GetType.ToString)
            End Try
        End Using
        Return UrlReplacements
    End Function

    Private Function UrlReplacementsToQueryString(ByVal dic As Dictionary(Of String, String)) As String
        Dim quer As String = String.Empty
        For Each dicEntry In dic
            quer = String.Format("{0}&{1}={2}", quer, dicEntry.Key, dicEntry.Value)
        Next
        Return quer
    End Function

End Class

Figure: Full source listing for the rule.

----------

TODO

What would I change and why…or things that I just did not have time to do.

TODO: Add more configurable parameters

The lack of meta data will lead to limitations in the future and ultimately the duplication of code. The ideal solution would be something like the ASP.NET SqlDataSource configuration, with a nice UI.

<asp:SqlDataSource ID="SqlDataSource1" runat="server" 
    CacheExpirationPolicy="Sliding" 
    ConnectionString="MyConnectionString" 
    EnableCaching="True" 
    SelectCommand="ssw_proc_SeoProductIdToProductKey" 
    SelectCommandType="StoredProcedure">
    <SelectParameters>
        <asp:RegexParameter DbType="StringFixedLength" DefaultValue="0" 
            Name="ProductId" RegexGroupName="ProductId" Size="100" Type="String" />
        <asp:Parameter DbType="StringFixedLength" Direction="Output" Name="ProductKey" 
            Size="255" Type="String" />
    </SelectParameters>
</asp:SqlDataSource>

Figure: Good Example, code from the ASP.NET 2.0 SqlDataSource.

You should be able to configure any set of input and output parameters.

TODO: Retrieve a record and replace based on the columns

It may make more sense to return a single record and perform the replaces based on the columns that are returned. This may help to reduce complexity while increasing functionality.

TODO: Add caching to improve performance

Caching is a difficult thing as it depends on the amount of data returned, but it can improve the speed.

 


Monday, December 28, 2009 #

On the project I am currently working on we want to change the nasty http://northwind.com/products.aspx?ProductId=1 to a nice friendly URL on the website. This is pretty easy and can result in nice URL’s like http://northwind.com/products/BigGreenTeddyBaresFromParis.aspx.

See Also – Solution - SEO permanent redirects for old URL’s?

Updated #1 January 5th, 2010: - As suggested by Adam Cogan, I changed the title and added a link to the Solution post.


This has already been implemented by the CMS system that we are using, so what is the problem?

The problem is that Google thinks the URL to “Big Green Teddy Bares from Paris” is http://northwind.com/products.aspx?ProductId=1 and we need to tell them that it is now http://northwind.com/products/BigGreenTeddyBaresFromParis.aspx. The URLs’ are changing for Search Engine Optimisation (SEO) reasons, but we do not want to loose any of the raking accumulated over time on the old URL’s.

We want to use Rewriting and not Routing because with rewriting the change is handled before it is passed to ASP.NET.

Rewriting in this case is like reverse URL Rewriting and during this process I need to lookup the database to find the new KEY (“BigGreenTeddyBaresFromParis”) and map the URL with the “ProductId” to the new “ProductKey”. If we also return a permanent redirect (301) then Google will learn the new location of the page and keep any ranking data associated with it intact. This is key as we do not want to start from scratch.

There are two official IIS7 rewrite engines that were recommended to me:

  • URL Rewrite
  • SEO Toolkit

URL Rewrite

You an install URL Rewrite from the “Web Platform Installer”, and it has very good integration and is easy to configure within IIS. This makes things a lot easier, but does it support 301 redirects?

image 
Figure: Adds an option right into the IIS interface

image
Figure: You can easily add new rules through the integrated UI

image 
Figure: UI supports 301 redirects, but it does not seam to have any way to load from a database.

Without a way to load from the database there is no way it will solve the problem, and a quick Google shows that it does not support it. The closest it can get is using a key value pair mapping file, but with 30,000 entries I do not think that will perform well.

If you look at Developing Rule Template for URL Rewrite Module you will see that you can only work within the set of options that are provided by the core functionality and can’t create a new feature, like loading the mappings from the database.

SEO Toolkit

Having looked at the bumph for the SEO Toolkit, it does not look like it provides any of the functionality required.

Conclusion

The conclusion is that neither the SEO Toolkit, nor the URL Rewrite Module are of any use in this case. There are now two options, I can roll my own rewriting framework or use another one that already exists that supports extensibility. One such URL rewriting framework that spring to mind is UrlRewritingNet.UrlRewrite which I have used before, but it has not been updated since April 2009. I have emailed the guys to ask them is they are still using/ working on it.

Even though it has not been updated since April 2009, I think this is the best option. The source code is provided on the site, and I am familiar with the component. It supports a rule provider model that will allow me to achieve the goal I am aiming for and is very easy to setup.


Monday, December 07, 2009 #

Previously I created this the manual way, but if you have a fast internet connection and can take the 1.6gb download of the AIK, then this is a much easier way of getting started.

This is not really the same as the SSW image that I created before, the SSW image was a lovely slipstreamed beauty with all the application I would ever need already preinstalled.. It was 32GB and took a very long time to setup. This will be sum what faster as it  only require a base Windows Server 2008 Setup…

  1. Download Windows® Automated Installation Kit (AIK)
    image
    Figure: Downloading AIK, done in under 5 minutes
    image 
    Figure: Unpack using WinRAR, don’t burn it as it is a waste of a DVD. Some applications don’t like being run from a Virtual DVD.
  2. Install  the AIK onto your local computer.
    image
    Figure: Weird setup, but what the heck…
  3. Download and Install Windows(R) Image to Virtual Hard Disk (WIM2VHD) Converter
    image
    Figure: Its just a script file that you download. I would prefer if it was a command line app with an optional UI, but you can’t have everything.
  4. Mount your Windows 2008 R2 image to a Virtual DVD drive. I am using Virtual Clone Drive.
    image
    Figure: Mounted Windows 2008 R2 ready to go…
    image
    Figure: Use your favourite Virtual DVD mounting software..
  5. coffee-cupRun the script to create your VHD using a command line running as an administrator
    CSCRIPT WIM2VHD.WSF /WIM:I:\sources\install.wim /SKU:SERVERSTANDARDCORE /VHD:D:\WimBuild\WinSvr2008R2OOB.vhd
    image 

You now have a lovely Out Of Box Windows 2008 R2 VHD. I would keep a copy of this in a nice safe place so you don’t need that coffee break every time.

Whoa there… not so fast..

Did you spot my mistake?

Could it be to do with the little “SERVERSTANDARDCORE” tag?

Could it be the “CORE” bit?

I think so.. I was just wondering why my resultant VHD was only 2.5gb in size!

Doh!

Lets try that properly:

  1. coffee-cupRun the script to create your VHD using a command line running as an administrator
    CSCRIPT WIM2VHD.WSF /WIM:I:\sources\install.wim /SKU:SERVERSTANDARD /VHD:D:\WimBuild\WinSvr2008R2OOB.vhd
    image

Make a copy of this file, and attach it to your boot list, and boot…


At SSW we are extensive users of Dynamics CRM. I wanted to give Office 2010 a go, but I had to make sure that the Dynamics CRM plug-in, and my other plug-ins worked.

You would think that support for Office 2010 Beta 2 was poor! You would be right and wrong…

I use a number of plug-ins for outlook:

  • LinkedIn
  • Plaxo
  • Team Companion
  • Dynamics CRM

All of them work…to an extent…

Outlook 2010 Beta 2 and Add-In’s: CRM, Team Companion, LinkedIn and Plaxo 
Figure: Screenshot of outlook with add-ins

Outlook 2010 put all of the Add-ins into a single tab called “Add-Ins” and they just get stacked up, which is bad!

image
Figure: Close up of the LinkedIn, Plaxo and Team Companion Add-ins

Can you see the problem? No? Well, the ribbon bar is only so tall, so that makes for 3 and only 3 add-ins. Where is the Dynamics CRM add-in? Can you see it in the first image? No! Let me help you.

image 
Figure: Where is Wally Dynamics CRM4 Add-in?

This looks useless, and it would be if the same options were not also available as a pull down menu.

image
Figure: Dynamics CRM4 pull down menu in Office 2010 have all the bits you need, even if you can’t get to the buttons.

The story is a little better when you open an email. The options for Dynamics CRM are prominent, as are the Team Companion and LinkedIn options.

image
Figure: Shows the Team Companion, LinkedIn and CRM options on an email; this is a much better format.

So, what else do you need to know? No 64-bit support yet, so you need to use Outlook 32-bit, and if you need to use Outlook 32-bit then you MUST use Office 32-bit:

    1. CRM4 will not Install if Office 2010 is installed
      Workaround: http://bovoweb.blogspot.com/2009/10/ms-outlook-2010-and-dynamics-crm.html
    2. If you upgrade Outlook 2007 to Outlook 2010 CRM will work
      http://dario.blog.viadis.hr/2009/07/outlook-2010-dynamics-crm-40-client.html
    3. You MUST use the 32bit version of Outlook 2010
      http://halo76.wordpress.com/2009/11/24/office-2010-and-crm-4-0-for-outlook-32-bit-only/
    4. If you are using Outlook 2010 32bit, the rest of the Office 2010 bits that you install must be of the same bitness
      http://blogs.msdn.com/officedevdocs/archive/2009/11/25/developing-outlook-2010-solutions-for-32-bit-and-64-bit-systems.aspx

This post also answers the question of wither you can move to Office 64-bit now? The answer is yes, unless you have any add-ins that you depend on and that do not work in Outlook 64-bit. If you do, you are in a bit of a pickle… Wait for support, or better yet, pester the Product team that makes your add-in to get it to support 64-bit office.

To Dynamics CRM Team, Plaxo Team, LinkedIn Team, TeamCompanion Team

Please can you:

  1. fix add-in to work with Outlook 64-bit (Team Companion guys are already on the case showing the rest of you up)
  2. fix add-in to have a ribbon tab like the Visual Studio ALM Add-in in Excel.
    image 

This being my first week at , and still waiting for my nice shiny new laptop to arrive, I am sitting here at my Wife’s laptop (which is PINK, a requirement to keep the WAF high), until it arrives.

 image 
Figure: Current workspace…one wall short of working in a cupboard, but it beats trying to work with the kids underfoot.

image
Figure: I know its nearly Christmas, but that's a long time between order and delivery!

SSW have sent me a .wim (Windows Image) file in the post and I really want to get a look at it before my new computer arrives.

In order to be able to create a clean install very quickly we need to convert this to a Windows 7 VHD. This way when the new computer arrives we can just move it over :) I also want to be able to reinstall my computer quickly. And what is quicker then mounting a new VHD and rebooting.

In order to achieve this there are a number of things that need done:

  1. Copy all of the .rar files from the DVD’s coffee-cup
    image
    Figure: First disk nearly finished

    image
    Figure: Third disk is taking a while
  2. Use WinRar to fit the 3 packages back together
    image
    Figure: Joining the wim file together is going to take a while as well. I don’t want to have to do this more than once!
  3. Create a new VHD
    image
    Figure: Showing the physical and Virtual disks on my wife's pink laptop.
  4. coffee-cupDeploy Image to new VDH
    In order to do this you will need imageX from the Windows 7 Automated Installation Kit. Check http://blogs.technet.com/aviraj/archive/2009/01/18/windows-7-boot-from-vhd-first-impression-part-2.aspx for more details and scenarios that will suit you.
    note: You may look at the Windows(R) Image to Virtual Hard Disk (WIM2VHD) Converter as another solution, but it requires that the Windows 7 Automated Installation Kit be installed locally, where I just downloaded imageX separately and bypassed the 1gb download.
    image
    Figure: As usual, this is showing the remaining in “Microsoft Minutes”

    image
    Figure: So 10% took just over a Minute? What is the rest of the hour for?

    image
    Figure: All done, I don’t know how long it took because I got on with some other things, but it was a while!
  5. Detach the VHD
     image
    Figure: Detaching the VHD will allow us to copy it.
  6. coffee-cupCopy the new VHD
    image
    Figure: This will allow me to save ssw.vhd for a rainy day, and use the copy as a working install.
  7. Rename the copy to “SSW_001.vhd”
  8. Attach SSW_001.vhd
    image
    Figure: Attaching a VHD is very easy

    image
    Figure:
  9. Add  the new SSW_001.vhd to the boot list using the folowing commands:
    C:\>bcdedit /copy {current} /d "SSW_001"
    C:\>bcdedit /set <guid> device vhd=[driveletter:]\<directory>\<vhd filename>
    C:\>bcdedit /set <guid> osdevice vhd=[driverletter:]\<directory>\<vhd filename>
    C:\>bcdedit /set <guid> detecthal on
    Note:  detecthal is used to force windows to auto detect the Hardware Abstraction Layer.
    image
    Figure: Added and configured the new Image…lets try it out…

Although this took a long time with 3 long running processes, it will be a lot faster next time as I can start from step #9…

Technorati Tags: ,,

I have been a cable customer in the UK since day one when it was Cable & Wireless.

If you don’t know who they are I am not surprised:

Cable & Wireless –> NTL –> VirginMedia

I received my first cable modem in 1998 (ish) which was 512 kbs.. much better than ye ole Dial-up, but while ADSL has lagged behind, cable have rocketed up to 50mbs this year.

With my new job at , I needed a faster internet connection. Ever since I moved house 5 years ago it has been a little slow, so I opted for Virgins 50mbit.

Now I thought it would be fast, but OMG:

image

I just downloaded Project 2010 in under 3 minutes… Did you notice the important figure in the image above?

Transfer Rate: 3.201 MB/sec

That's not megabit! that's Megabyte"!

Here is the speed in your flavour of choice:
=25.608 Mbps [Megabit-per-second]
=25608 Kbps [Kilobit-per-second]
=3.201 MB/sec [Megabyte-per-second]
=3201 KB/sec [Kilobyte-per-second]

http://www.mediaroad.com/products/speedcheck/free_tools/unit_convert/

And that's not the fastest it was ripping Project from the Microsoft site, it was just the speed when my PrtScr kicked in… it was up around the 4.xx mark!

We have come a long way since 56k in such a short time, and I am loving it….

(Update 06/12/2009)

Before upgrade (was on 20mbs):

SpeedTest.net Before (2)

After Upgrade (now on 50mbs):

644659208 

(Update 07/12/2009)

it just gets better and better:

image

Technorati Tags: ,,

Friday, December 04, 2009 #

http://blog.hinshelwood.com/archive/2009/12/07/internet-connection-speed-wow-again.aspx

Double posted in error please use link above...


Thursday, November 12, 2009 #

I have been trying since SP1 was released to get it installed at Aggreko, but due to our global, three time zones, development team and release schedules it has been very difficult to get some time set aside for it.

Now that I am leaving, last day is Tuesday 17th November, there was more of an apatite to take the hit on time and get it installed.

While I may be late to the game for SP1, I was conscious that a lot of gotchas around the installation had been reported when it was released.

You can find a full list on Brian Harry's blog on his Problems installing TFS SP1 post, but I have to say that I have never had an install, except maybe 2010, go more smoothly. Its always the same when you take lots of precautions for Murphy's Law to rear its head, nothing goes wrong ;).

We have a single virtual server instance of TFS with the only architectural customisation is the link between TFS and our corporate MOSS environment.

Release Plan

  1. Turn off remote access to TFS websites
    image
  2. Verify access to TFS is not possible remotely
    image
  3. Run full SQL backup
    image
  4. Take a snapshot (VM Ware) of the TFS server [Infrastructure Team]
  5. Install VS2008 SP1 if client installed
    image
  6. Install TFS2008 Service Pack 1
    image
    If any problems are encountered refer to Brian Harry’s post on resolving SP1 install issues: http://blogs.msdn.com/bharry/comments/1627061.aspx
  7. Follow test plan
  8. If tests fail, follow back out plan
  9. Done

Test Plan

  1. Check event log for errors
    image
  2. Check all services are running
    image
  3. Test web access
    image
  4. Test Visual Studio Access
    image

Back out Plan

1. Restore last snapshot

2. Start TFS website in IIS

3. Test TFS Services by connecting through Visual Studio 2005 / 2008

4. Test Web Access (http://tfs01.northwind.com)

Conclusion

Although there seemed to be a lot of noise around the time that SP1 was released, the great god Murphy left me alone in this instance. It just goes to show, simpler is better...


Monday, November 02, 2009 #

Its "Dyslexia Awareness Week" here in the UK, and as a person that benefits from being a Dyslexic developer, I thought I should highlight the specific strengths to programmers of being dyslexic...

All of the benefits are due to a neurological difference that presents as a larger right-hemisphere in the brain and many more neural connections are formed than is normally found. While this can make it difficult for others to follow the actual thought process the benefits outweigh the cost of this and the random symbol orientation problems that most dyslexic people suffer from:

  • 3-D visualization ability
  • creative problem solving skills
  • intuitive people skills
  • visually interpreting information in 3d while applying a 4th dimension, reasoning. (e.g. value, logic, action, purpose, possibility, personality, emotion, sentiment and action, etc.)

If only our education systems would take advantage of these differences...

British Dyslexia Association: Dyslexia Awareness Week: Dyslexia Strenths


Sunday, October 25, 2009 #

Well, nothing like hitting the ground running, my first job at SSW was to join the TFS Migration Team, it was a fun experience, let me tell you how it went.

Update #1 20th January 2010: Have a look at our Rules to better TFS2010 Migration 


Adam put a few guys together:

  • Adam Cogan (Australia) – The team lead who checks everything and makes us follow the rules to better TFS.
  • Eric Phan (Australia) – Created an excellent "Rules to a successful migration from TFS 2008 to TFS 2010 guide”
  • Justin King (Australia) – Justin seems to play the part of devil’s advocate. I looked him up in the company directory and he is a previous employee…I guess you never really leave SSW.
  • Me (Scotland) – The implementer
  • Allan Zhou (Beijing) – My co-conspirator for the implementation

We started at 2:30am (GMT+1) on Saturday morning and we did it in 5 major steps:

  1. Backed up TFS 2008 databases (Some 14GB of data)
  2. Restored databases to new 64 bit server
  3. Installed TFS 2010 Beta 2 64 bit
  4. Run the Upgrade of 2008 data to 2010 Beta 2
  5. Tested the deployment

We completed the migration at 9:15am (GMT+1) on Saturday morning so all in the migration took just less than 7 hours.

 image

Figure: Web Access – Working

VS2010

Figure: Visual Studio - Working

 

Well done to the SSW team.

Well done also to the guys involved in the TFS team, the same migration from TFS 2005 to TFS 2008 was a much more painful experience taking days of work, but the guys from SSW made this process easy and straight forward…Preparation does that for a project…

A possible claim to fame: In addition we might have been the first company (SSW is a company of 52 employees and contractors) to migrate. So far I have not seen any blog posts about other companies migrating everything over to Beta 2. I am a TFS MVP and no-one on that list has posted about a migration yet (I can just imagine Justin King having another fit when he finds that out).

            

 


Need Help?

www.SSW.com.au

SSW was the first company in the world outside of Microsoft to deploy Visual Studio 2010 Team Foundation Server to production, not once, but twice.

Team Foundation Server

Visual Studi2010 ALM SSW provides expert Visual Studio ALM guidance including installation, configuration and customisation through our four Microsoft Visual Studio ALM MVP’s in three countries; Australia, Beijing and the UK. They have experience deploying to small development shops all the way through to large blue chips.

Professional Scrum Developer Training

Professional Scrum Developer Training SSW has six Professional Scrum Developer Trainers who specialise in training your developers in implementing Scrum with Microsoft's Visual Studio ALM tools.


In the last 2+ years at Aggreko I have worked with Visual Studio 2008 Team Foundation Server, Office SharePoint Server 2007 and a number of WPF, Silverlight and ASP.NET projects.

There had been some discussion of a new role within Aggreko in the solution architecture arena. I also spoke to Adam Cogan who has the title “SSW Chief Architect and Microsoft Regional Director”…

This fortuitous communication, which turned into an interview, resulted in an offer from Adam Cogan of employment as a Senior Software Architect at SSW

I got through the interview and I decided to take a role at SSW…

SSWLogo

If you know of Adam, then you will know that he has rules and standards for everything. If you have not heard of him, then I suggest that you have a read of those rules and see what they are all about.

The first set of rules that I read was the Rules to better Email and they helped me be more productive even before I accepted the job…

Check out rule #32 in SSW’s “Rules To Being Software Consultants Working In A Team

#32 Do you enjoy your job?

The expectation from Adam is:

  • #1 is to put your heart into your job and enjoy yourself
  • Get your Employee Responsibilities (Scheduled recurring events) done
  • Improve SSW to a better place every week
  • Improve yourself to better person every week

If you find yourself not enjoying your job this is not necessarily a bad thing. You should make a commitment to give it a go and try to make it work. When you have decided you are unhappy you should talk to your boss and figure out what is making you unhappy. The fact is that there are some jobs that you are not suited to. It is probably best for everyone that you start to think about moving on and trying something that may make you happier.

I totally agree with this and at Aggreko I was supported by many people. I spoke to my boss Andre Vermeulen about the things I was not happy with, and we came to an understanding, but it is difficult for a large company to move at the same pace that I do. I found working with SharePoint 2003 is really just unacceptable.

In my new role at SSW I will be tasked with:

  • bringing SSW’s rules to European clients
  • helping organisations be more proactive with the Visual Studio 2010 ALM offering,
  • migrating TFS 2005 and TFS 2008 customers to the joys of TFS 2010
  • enabling SSW to have 24 hour operations

On top of this I will be using SharePoint 2010 and CRM 2005 in order to implement intranets and CRM for clients.

Its going to be a fun ride, and if you want to take your company to the next step and you are in Europe, please contact me.

 

 

 

 

If you get a chance, check out SSW’s Rules. I am sure you will find something that will make you more productive and happier…

RulestoBetter


Tuesday, October 20, 2009 #

As Microsoft have separated Install with configuration, so I have separated my posts! You will need TFS2010 installed prior to the steps below.

clip_image001

This is my configuration experience...This wizard is excellent. If you had ever tried to install TFS in the past and it taken you a long time (took me 7 days the first time in 2005) Then you need to give this a go...

Team Foundation Server Configuration

You can pick basic and it is...well...basic. It will install everything to the defaults.

Team Foundation Server Configuration - Advanced

I'm picking Advanced because I want to be able to select a pre-existing SQL Express instance...

Team Foundation Server Configuration - Advanced - Install

Team Foundation Server Configuration - Advanced - Database

You can enter a label if you want to have more than one TFS Configuration database in the same SQL instance.

Team Foundation Server Configuration - Advanced - Account

If you are wanting to run on a network, maybe with an externally accessible URL, then you may need to pay attention to the security, but I don't really care for this install... Team Foundation Server Configuration - Advanced - Application Tier

If you want to ever be able to connect Visual Studio 2005 clients to the server you MUST remove the virtual directory as Team Explorer 2005 will not be able to anything but the default collection.

Team Foundation Server Configuration - Advanced - Project Collection

Ok, I have a default collection, but only because I am lazy...

Team Foundation Server Configuration - Advanced - Review All done, now to apply it.

 Team Foundation Server Configuration - Advanced - Rediness Checks

No, wait, we need to check all of the system requirements!

Team Foundation Server Configuration - Advanced - Configure

Now, usually this is the time to break out a cup of team, and maybe have a siesta. Lets see how long it takes...

Team Foundation Server Configuration - Advanced - Configure after 20 seconds

..30 seconds...

Team Foundation Server Configuration - Advanced - Configure after 40 seconds

...50 seconds...

Team Foundation Server Configuration - Advanced - Configure after 50 seconds

.. 1 minute...

Team Foundation Server Configuration - Advanced - Configure after 60 seconds

..Whoa, that was less than 2 minutes for the whole process.

Team Foundation Server Configuration - Advanced - Configure - Complete Just to prove that this whole process took less than 12 minutes, here is the beginning and end of the log file:

[Info   @12:06:41.111] ====================================================================
[Info   @12:06:41.183] Team Foundation Server 2010 Administration Log
[Info   @12:06:41.186] Version  : 10.0.21006.1
[Info   @12:06:41.203] DateTime : 10/20/2009 13:06:41
[Info   @12:06:41.203] Type     : Configuration
[Info   @12:06:41.206] Activity : Deploy
[Info   @12:06:41.208] Area     : ApplicationTier
[Info   @12:06:41.216] User     : DOMAIN\martihins
[Info   @12:06:41.216] Machine  : ED0919
[Info   @12:06:41.229] System   : Microsoft Windows NT 6.0.6002 Service Pack 2 (AMD64)
[Info   @12:06:41.229] ====================================================================

... shortened ...

[Info   @12:18:28.147] Ending the Install operation on the ApplicationTier tier.
 

Whoa, that was fast! Compared to previous versions I was done before I started, like crossing an international date line. Another one is... no documentation... nope, I didn't look at it once! I would not recommend this approach, at least have a look to make sure you are installing the correct version on the correct URL's and to learn what the terms are.

P.S. Visual Studio 2005 and Visual Studio 2008 any version without the Team Foundation Server 2010 compatibility pack WILL NOT CONNECT! The Visual Studio Team System 2008 Service Pack 1 Forward Compatibility Update for Team Foundation Server 2010 is available, but 2005 will not be available until RTM.

Visual Studio Team System 2008 Service Pack 1 Forward Compatibility Update for Team Foundation Server 2010

I should note that you should not complain about the limited support for 2005. Microsoft expects the install base to be less than 5% by the time Visual Studio 2010 is released, and they were not going to support it at all. That there is any support at all is due to the lobbying of the Team System MVP community and TAP customers and excelent communication with the product teams...


New in Visual Studio 2010 is the ability to install TFS on XP, Vista and Windows 7. You can use SQL 2008 Express, so no large overhead, and the Basic version you use for this does have the reporting and SharePoint requirement that the main install does. That does not mean that you can't upgrade later :)

image

Once you have TFS2010 installed you will need to configure it...

New logo, new install. Microsoft have changed the, lets face it, horrible install, and split it into two separate pieces. Install and Configuration.

First The Install: The only options are wither you install server and build... nice...

Microsoft Team Foundation Server 2010 Install

Microsoft Team Foundation Server 2010 Install - Start Page

 

Microsoft Team Foundation Server 2010 Install - Options Page

Microsoft Team Foundation Server 2010 Install - Install Page

Microsoft Team Foundation Server 2010 Install - Finish Page

Total install time: 3 minutes (which includes the time to take these screenshots and save them)

 

Now that I have TFS2010 installed I will need to configure it...


I was recently contacted by Colin Mackay, the chairman of Scottish Developers about doing an interview with them. Colin has been pestering me for a while now to do some speaking engagements, but I am still not comfortable with that! (Yes, I am too chicken), so I capitulated…

My interview appears in the October edition of their newsletter and although I think I rambled a little, understatement of the year, I do think I came across ok, if a little scatter brained…

 


Monday, October 19, 2009 #

Visual Studio 2010 Beta 2 is now available on MSDN for download!

clip_image001

With 2010 comes new SKU's. Microsoft is trying to simplify the layout and features that you can get.

Visual Studio IDE now comes in these flavours:

· Microsoft® Visual Studio® 2010 Professional

· Microsoft® Visual Studio® 2010 Professional with MSDN

· Microsoft® Visual Studio® 2010 Premium with MSDN

· Microsoft® Visual Studio® 2010 Ultimate with MSDN

clip_image002

So no Team Edition, and bits of Team Suit has been split between Premium and Ultimate. I addition all of the editions above will include a Team Foundation Server CAL which will make licensing a lot simpler.

Although Premium and Ultimate will continue to be in the ALM space there are also other elements like the Test Elements and Lab management that are new for 2010 that also sit in this space with Team Foundation Server.

· Microsoft® Visual Studio® Test Elements 2010 with MSDN

· Microsoft® Visual Studio® Team Foundation Server 2010

· Microsoft® Visual Studio® Team Lab Management 2010

If you have not already heard there will be a Team Foundation Server Express product that is able to be installed on Vista and Windows 7.

Check out the new channel 9 videos:

10-4 Episode 33: Downloading and Installing Visual Studio 2010 Beta 2

How to create record and playback Test Cases in Visual Studio Beta2

Technorati Tags: ALM,Visual Studio ALM


Monday, August 31, 2009 #

Although this post is called Scale Transform Behaviour you could use any transform / animation in its place. The purpose is to have a slider control in a menu be able to alter the scale of any number of controls within MVVM views.

image

This behaviour allows you to add any Framework Elements to a list of attached controls by adding an attached property of GlobalScaleTransformBehaviour.IsScaled to your controls.

Public Class GlobalScaleTransformBehaviour

    Private Shared sm_AttachedControls As List(Of FrameworkElement)
    Public Shared ReadOnly IsScaledProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsScaled", GetType(Boolean), GetType(GlobalScaleTransformBehaviour), New UIPropertyMetadata(False, New PropertyChangedCallback(AddressOf GlobalScaleTransformBehaviour.IsScaledChanged)))
    Private Shared sm_CurrentScale As Double = 1

    Shared Sub New()
        sm_AttachedControls = New List(Of FrameworkElement)
    End Sub

    Public Shared Function GetIsScaled(ByVal element As DependencyObject) As Boolean
        If element Is Nothing Then
            Throw New ArgumentNullException("element")
        End If

        Return element.GetValue(IsScaledProperty)
    End Function

    Public Shared Sub SetIsScaled(ByVal element As DependencyObject, ByVal value As Boolean)
        If element Is Nothing Then
            Throw New ArgumentNullException("element")
        End If
        element.SetValue(IsScaledProperty, value)
    End Sub

    Private Shared Sub IsScaledChanged(ByVal obj As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
        Dim itemToResize As FrameworkElement = TryCast(obj, FrameworkElement)
        If (Not itemToResize Is Nothing) Then
            If Object.Equals(e.NewValue, True) Then
                sm_AttachedControls.Add(itemToResize)
                itemToResize.LayoutTransform = New ScaleTransform(sm_CurrentScale, sm_CurrentScale)
            Else
                sm_AttachedControls.Remove(itemToResize)
                itemToResize.LayoutTransform = New ScaleTransform(1, 1)
            End If
        End If
    End Sub

End Class

 

As you can see, there is an attached dependency Boolean property defined with a PropertyChangedCallback. When the PropertyChangedCallback method is called we test to see if it is a True or False value and either add the control to a static list and set the current Transform, or remove the control from the list and reset the transform to 1.

This works grate and you can manipulate the list of controls at runtime by changing the dependency property.

<igWindows:TabItemEx 
    xmlns:igDP="http://infragistics.com/DataPresenter"     
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:local="clr-namespace:Hinshlabs.WpfHeatItsmDashboard"
    xmlns:igWindows="http://infragistics.com/Windows"
    xmlns:igDock="http://infragistics.com/DockManager"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
    mc:Ignorable="d" 
    xmlns:igEditors="http://infragistics.com/Editors"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
    xmlns:ic="clr-namespace:Microsoft.Expression.Interactivity.Core;assembly=Microsoft.Expression.Interactions" 
    x:Class="CallsView" x:Name="CallsView" MinWidth="30" MinHeight="50">
    <igWindows:TabItemEx.Resources>
        <local:NinjectDataProvider  
        x:Key="ViewModel" 
        d:IsDataSource="True" ObjectType="{x:Type local:CallsViewModel}"  
        />
       <local:DateTimeSecondsToBooleanConverter x:Key="DateTimeSecondsToBooleanConverter" />
    </igWindows:TabItemEx.Resources>
    <igWindows:TabItemEx.Triggers>
        <EventTrigger RoutedEvent="FrameworkElement.Loaded"/>
    </igWindows:TabItemEx.Triggers>
    <igWindows:TabItemEx.Header>
        <igEditors:XamTextEditor Text="{Binding Source={StaticResource ViewModel},Path=Header, diag:PresentationTraceSources.TraceLevel=High}" />
    </igWindows:TabItemEx.Header>

    <DockPanel local:GlobalScaleTransformBehaviour.IsScaled="True" DataContext="{Binding Source={StaticResource ViewModel}}">
        <Border DockPanel.Dock="Top" Background="LightGray" MinHeight="20">
        <Border.Style>
            <Style>
                <Style.Triggers>
                  <DataTrigger Binding="{Binding Source={StaticResource ViewModel},Path=IsLoading, diag:PresentationTraceSources.TraceLevel=High}" Value="False">
                           <Setter Property="Border.Visibility" Value="Collapsed" />
                  </DataTrigger>
                    </Style.Triggers>
            </Style>
        </Border.Style>
            <Label Content="Loading data..." />
        </Border>
        <Border DockPanel.Dock="Top" Background="LightGray" MinHeight="20">
            <Border.Style>
                <Style>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Source={StaticResource ViewModel},Path=IsSyncing, diag:PresentationTraceSources.TraceLevel=High}" Value="False">
                            <Setter Property="Border.Visibility" Value="Collapsed" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Border.Style>
            <Label Content="Syncing data..." />
        </Border>
        <igDP:XamDataGrid DataSource="{Binding Calls}" Theme="Office2k7Blue">
            <igDP:XamDataGrid.Resources>
                <Style x:Key="{x:Type igDP:DataRecordCellArea}" TargetType="{x:Type igDP:DataRecordCellArea}">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding DataItem.TypeOfCall, Converter={StaticResource DateTimeSecondsToBooleanConverter}, ConverterParameter=1}" Value="True">
                            <Setter Property="Background">
                                <Setter.Value>
                                    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                        <LinearGradientBrush.GradientStops>
                                            <GradientStopCollection>
                                                <GradientStop Offset="0" Color="Red"/>
                                                <GradientStop Offset="1" Color="Green"/>
                                            </GradientStopCollection>
                                        </LinearGradientBrush.GradientStops>
                                    </LinearGradientBrush>
                                </Setter.Value>
                            </Setter>
                        </DataTrigger>
                </Style.Triggers>
            </Style>
                </igDP:XamDataGrid.Resources>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowRecordFiltering="true" FilterEvaluationTrigger="OnCellValueChange"  AllowSummaries="True" FilterOperatorDropDownItems="All" />
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings AutoGenerateFields="true" FilterUIType="LabelIcons" />
            </igDP:XamDataGrid.FieldLayoutSettings>
        </igDP:XamDataGrid>
        </DockPanel>
</igWindows:TabItemEx>

There is quite a lot of Wpf here, so I have highlighted the DockPanel to which the dependency has been applied. All we now need to do is provide a way to manipulate this value. We need to add a ScaleValue attached dependency property to our Behaviour that we can bind to our single or set of control controls.

Public Class GlobalScaleTransformBehaviour

    Private Shared sm_AttachedControls As List(Of FrameworkElement)
    Public Shared ReadOnly IsScaledProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsScaled", GetType(Boolean), GetType(GlobalScaleTransformBehaviour), New UIPropertyMetadata(False, New PropertyChangedCallback(AddressOf GlobalScaleTransformBehaviour.IsScaledChanged)))
    Public Shared ReadOnly ScaleValueProperty As DependencyProperty = DependencyProperty.RegisterAttached("ScaleValue", GetType(Double), GetType(GlobalScaleTransformBehaviour), New UIPropertyMetadata(CType(1, Double), New PropertyChangedCallback(AddressOf GlobalScaleTransformBehaviour.ScaleValueChanged)))
    Private Shared sm_CurrentScale As Double = 1

    Shared Sub New()
        sm_AttachedControls = New List(Of FrameworkElement)
    End Sub

    Public Shared Function GetIsScaled(ByVal element As DependencyObject) As Boolean
        If element Is Nothing Then
            Throw New ArgumentNullException("element")
        End If

        Return element.GetValue(IsScaledProperty)
    End Function

    Public Shared Sub SetIsScaled(ByVal element As DependencyObject, ByVal value As Boolean)
        If element Is Nothing Then
            Throw New ArgumentNullException("element")
        End If
        element.SetValue(IsScaledProperty, value)
    End Sub

    Private Shared Sub IsScaledChanged(ByVal obj As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
        Dim itemToResize As FrameworkElement = TryCast(obj, FrameworkElement)
        If (Not itemToResize Is Nothing) Then
            If Object.Equals(e.NewValue, True) Then
                sm_AttachedControls.Add(itemToResize)
                itemToResize.LayoutTransform = New ScaleTransform(sm_CurrentScale, sm_CurrentScale)
            Else
                sm_AttachedControls.Remove(itemToResize)
                itemToResize.LayoutTransform = New ScaleTransform(1, 1)
            End If
        End If
    End Sub

    Public Shared Function GetScaleValue(ByVal element As DependencyObject) As Double
        If element Is Nothing Then
            Throw New ArgumentNullException("element")
        End If

        Return element.GetValue(ScaleValueProperty)
    End Function

    Public Shared Sub SetScaleValue(ByVal element As DependencyObject, ByVal value As Double)
        If element Is Nothing Then
            Throw New ArgumentNullException("element")
        End If
        element.SetValue(ScaleValueProperty, value)
    End Sub

    Private Shared Sub ScaleValueChanged(ByVal obj As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
        If Not Application.Current.Dispatcher.CheckAccess Then
            Exit Sub
        End If
        sm_CurrentScale = e.NewValue
        SyncLock sm_AttachedControls
            For Each itemToResize In sm_AttachedControls.ToList
                ' Apply Tensform
                itemToResize.LayoutTransform = New ScaleTransform(sm_CurrentScale, sm_CurrentScale)
            Next
        End SyncLock
    End Sub

End Class

This value is stored so we can set new controls, and then applied to all of the currently attached controls. I have chosen to bind to a slider, but any way of passing in the required values is just fine.

<igRibbon:XamRibbonWindow x:Class="MainWindowView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:igRibbon="http://infragistics.com/Ribbon"
    xmlns:igEditors="http://infragistics.com/Editors"
    xmlns:igWindows="http://infragistics.com/Windows"
    xmlns:igDock="http://infragistics.com/DockManager"
    xmlns:local="clr-namespace:Hinshlabs.WpfHeatItsmDashboard"
    Title="Heat Itsm Dashboard" MinHeight="600" MinWidth="800" Icon="/Hinshlabs.WpfHeatItsmDashboard;component/HeatItsm.ico">
    <igRibbon:XamRibbonWindow.Resources>
        <local:NinjectDataProvider 
        x:Key="ViewModel" 
        ObjectType="{x:Type local:MainWindowViewModel}" 
        />
    </igRibbon:XamRibbonWindow.Resources>
    <igRibbon:RibbonWindowContentHost DataContext="{StaticResource ViewModel}">
        <igRibbon:RibbonWindowContentHost.Ribbon>
            <igRibbon:XamRibbon local:XamRibbonBehaviour.IsEntryPoint="True" DockPanel.Dock="Top" AutoHideEnabled="True" Theme="Office2k7Blue" >
            <igRibbon:XamRibbon.ApplicationMenu>
                    <igRibbon:ApplicationMenu RecentItemsHeader="{Binding Resources.RecentItemsHeader}" Image="/Hinshlabs.WpfHeatItsmDashboard;component/Images/heat.gif">
                        <igRibbon:ButtonTool Caption="Update" />
                    <igRibbon:ApplicationMenu.FooterToolbar>
                        <igRibbon:ApplicationMenuFooterToolbar>
                            <igRibbon:ButtonTool Command="{Binding ExitCommand}" Caption="{Binding Resources.ExitButtonCaption}"/>
                        </igRibbon:ApplicationMenuFooterToolbar>
                    </igRibbon:ApplicationMenu.FooterToolbar>
                        </igRibbon:ApplicationMenu>
                </igRibbon:XamRibbon.ApplicationMenu>
                <igRibbon:XamRibbon.Tabs>
                    <igRibbon:RibbonTabItem Header="{Binding Resources.Ribbon_HomeTab_Header}">
                        <igRibbon:RibbonGroup Caption="{Binding Resources.Ribbon_HomeTab_ViewsGroup_Caption}">
                            <igRibbon:ToolHorizontalWrapPanel>
                                <igRibbon:ButtonTool Caption="{Binding Resources.Ribbon_HomeTab_ViewsGroup_CallsViewButtonCaption}" Command="{Binding AddCallsViewCommand}" />
                            </igRibbon:ToolHorizontalWrapPanel>
                        </igRibbon:RibbonGroup>
                        <igRibbon:RibbonGroup Caption="{Binding Resources.Ribbon_HomeTab_OptionsGroup_Caption}">
                                    <igRibbon:ToolHorizontalWrapPanel>
                                    <igRibbon:ButtonGroup>
                                        <igRibbon:ToggleButtonTool IsChecked="{Binding FickEnabled, Mode=TwoWay}" Content="{Binding Resources.Ribbon_HomeTab_OptionsGroup_Flick_ToggleButton_Caption}"/>
                                </igRibbon:ButtonGroup>
                                    </igRibbon:ToolHorizontalWrapPanel>
                            <igRibbon:ToolHorizontalWrapPanel>
                                <igRibbon:ButtonGroup>
                                    <Label Content="Scale" />
                                    <Slider Minimum="0.5" Maximum="3" Width="200" local:GlobalScaleTransformBehaviour.ScaleValue="1" LargeChange=".5" SmallChange=".1"  Value="{Binding Path=(local:GlobalScaleTransformBehaviour.ScaleValue),RelativeSource={RelativeSource Self}, Mode=TwoWay}">
                                    </Slider>
                                </igRibbon:ButtonGroup>
                            </igRibbon:ToolHorizontalWrapPanel>
                        </igRibbon:RibbonGroup>
                    </igRibbon:RibbonTabItem>
                </igRibbon:XamRibbon.Tabs>
            </igRibbon:XamRibbon>
    </igRibbon:RibbonWindowContentHost.Ribbon>
        <AdornerDecorator>
        <DockPanel>
            <local:UpdateView DockPanel.Dock="Top" />
            <igWindows:XamTabControl TabItemCloseButtonVisibility="Visible" TabStripPlacement="Top" ItemsSource="{Binding CallsViews}" SelectedItem="{Binding SelectedCallsView}" local:TabControlTimedBehaviour.IsTimedCycle="{Binding FickEnabled}" Theme="Office2k7Blue">
            </igWindows:XamTabControl>
        </DockPanel>
        </AdornerDecorator>
    </igRibbon:RibbonWindowContentHost>
</igRibbon:XamRibbonWindow>
image

As you can see I am heavily utilizing the Infragistics controls, but that would not affect this procedure. The result is the ability to smoothly scale your controls based on a global scale setting.

image

 

 

 

krsu46zvpt