Monday, February 08, 2010
<rant>
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:
vodafone,
fail </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:<martin@hinshelwood.com> 0 32 SMTP - -
127.0.0.1 MYHOST-MYSERVER SMTPSVC1 MYHOST-MYSERVER 127.0.0.1 0 DATA <MYHOST-MYSERVERVoMDrx0000015e@MYHOST-MYSERVER> 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:<enquiries@company.com>+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:<martin@hinshelwood.com> 0 0 SMTP - -
216.146.33.4 OutboundConnectionResponse SMTPSVC1 MYHOST-MYSERVER - 25 - 504+5.5.2+<MYHOST-MYSERVER>:+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:<enquiries@company.com>+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:
- I opened “Internet Information Services (IIS) 6.0 Manager” on the server.
- Expanded and then right click on “[SMTP Virtual Server #1]” and select “Properties”
- Select the “Delivery” Tab and then “Advanced”
- Enter “company.com” in the “fully-qualified domain name” field.
- 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:
IIS7,
SMTP,
Email,
5.5.2
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
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?
Figure: Adds an option right into the IIS interface
Figure: You can easily add new rules through the integrated UI
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…
- Download Windows® Automated Installation Kit (AIK)
Figure: Downloading AIK, done in under 5 minutes
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. - Install the AIK onto your local computer.
Figure: Weird setup, but what the heck… - Download and Install Windows(R) Image to Virtual Hard Disk (WIM2VHD) Converter
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. - Mount your Windows 2008 R2 image to a Virtual DVD drive. I am using Virtual Clone Drive.
Figure: Mounted Windows 2008 R2 ready to go…
Figure: Use your favourite Virtual DVD mounting software..
Run 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
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:
Run 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
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…
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!
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.
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.
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.
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:
- CRM4 will not Install if Office 2010 is installed
Workaround: http://bovoweb.blogspot.com/2009/10/ms-outlook-2010-and-dynamics-crm.html - 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 - 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/ - 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:
- fix add-in to work with Outlook 64-bit (Team Companion guys are already on the case showing the rest of you up)
- fix add-in to have a ribbon tab like the Visual Studio ALM Add-in in Excel.
This being my first week at SSW, 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.
Figure: Current workspace…one wall short of working in a cupboard, but it beats trying to work with the kids underfoot.
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:
- Copy all of the .rar files from the DVD’s
Figure: First disk nearly finished
Figure: Third disk is taking a while
- Use WinRar to fit the 3 packages back together
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!
- Create a new VHD
Figure: Showing the physical and Virtual disks on my wife's pink laptop.
Deploy 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.
Figure: As usual, this is showing the remaining in “Microsoft Minutes”
Figure: So 10% took just over a Minute? What is the rest of the hour for?
Figure: All done, I don’t know how long it took because I got on with some other things, but it was a while!
- Detach the VHD
Figure: Detaching the VHD will allow us to copy it.
Copy the new VHD
Figure: This will allow me to save ssw.vhd for a rainy day, and use the copy as a working install.
- Rename the copy to “SSW_001.vhd”
- Attach SSW_001.vhd
Figure: Attaching a VHD is very easy
Figure:
- 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.
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:
Windows 7,
VHD,
WIM
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 SSW, 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:
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):
After Upgrade (now on 50mbs):
(Update 07/12/2009)
it just gets better and better:
Friday, December 04, 2009
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.
Turn off remote access to TFS websites
Verify access to TFS is not possible remotely
Run full SQL backup
Take a snapshot (VM Ware) of the TFS server [Infrastructure Team] Install VS2008 SP1 if client installed
Install TFS2008 Service Pack 1

If any problems are encountered refer to Brian Harry’s post on resolving SP1 install issues: http://blogs.msdn.com/bharry/comments/1627061.aspx Follow test plan If tests fail, follow back out plan Done
Check event log for errors
Check all services are running
Test web access
Test Visual Studio Access
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)
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...

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:
- Backed up TFS 2008 databases (Some 14GB of data)
- Restored databases to new 64 bit server
- Installed TFS 2010 Beta 2 64 bit
- Run the Upgrade of 2008 data to 2010 Beta 2
- 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.
Figure: Web Access – Working
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).
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…

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…
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…

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.

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...

You can pick basic and it is...well...basic. It will install everything to the defaults.
I'm picking Advanced because I want to be able to select a pre-existing SQL Express instance...

You can enter a label if you want to have more than one TFS Configuration database in the same SQL instance.
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... 
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.
Ok, I have a default collection, but only because I am lazy...
All done, now to apply it.
No, wait, we need to check all of the system requirements!
Now, usually this is the time to break out a cup of team, and maybe have a siesta. Lets see how long it takes...
..30 seconds...
...50 seconds...
.. 1 minute...
..Whoa, that was less than 2 minutes for the whole process.
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 :)
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...



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!

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

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.
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>
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.
krsu46zvpt
Tuesday, August 25, 2009
You have probably heard me go on about Unity a couple of times:
I have been using what is now unity since the good old days (sooo not true, WPF is the Windows Forms killer, and good riddance) of WindowsForms and CAB (Client Application Block), but now there is a lightweight alternative: Ninject.
I decided on my latest project (a Wpf dashboard for HEAT ITSM) that I needed dependency injection. Whenever I start building a MVVM project I always end up needing some sort of dependency injection to keep everything nice and neat. It is only really needed once you get to a certain size and when you start wanting talk between ViewModels.
Anyway I was using a method of injecting my ViewModels into the Views using standard binding:
<igDock:ContentPane x:Class="SlaTodayView"
xmlns:igDP="http://infragistics.com/DataPresenter"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:WpfHeatItsmDashboard"
xmlns:igWindows="http://infragistics.com/Windows"
xmlns:igDock="http://infragistics.com/DockManager"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Header="Sla Today" MinWidth="30" MinHeight="50">
<igDock:ContentPane.Resources>
<ObjectDataProvider
x:Key="ViewModel"
ObjectType="{x:Type local:SlaTodayViewModel}"
/>
</igDock:ContentPane.Resources>
<igDP:XamDataGrid DataContext="{StaticResource ViewModel}" DataSource="{Binding Calls}" Theme="Office2k7Black" >
</igDP:XamDataGrid>
</igDock:ContentPane>
But once you move to dependency injection you do not want to keep all those fixed object definitions. These may become interfaces, or you may just want to replace, or dynamically replace, one of these types by a derived one at runtime.
That being the goal, we need some way to retrieve that type even in design mode. There is nothing worse than components or bits of components that make it difficult to work in both Visual Studio and Blend, and with the new binding features of Visual Studio 2010 for WPF 4 it will be even more important that your usage is as compatible as possible.
What I decided to do was create a custom DataSourceProvider, called the NinjectDataProvider that I could use instead of the ObjectDataProvider. This is the first version of that provider and it does nothing more than retrieve the type form the Ninject Kernel. Minimal changes to the WPF enable this:
<igDock:ContentPane x:Class="SlaTodayView"
xmlns:igDP="http://infragistics.com/DataPresenter"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:local="clr-namespace:WpfHeatItsmDashboard"
xmlns:igWindows="http://infragistics.com/Windows"
xmlns:igDock="http://infragistics.com/DockManager"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Header="Sla Today" MinWidth="30" MinHeight="50">
<igDock:ContentPane.Resources>
<local:NinjectDataProvider
x:Key="ViewModel"
ObjectType="{x:Type local:SlaTodayViewModel}"
/>
</igDock:ContentPane.Resources>
<igDP:XamDataGrid DataContext="{StaticResource ViewModel}" DataSource="{Binding Calls}" Theme="Office2k7Black" >
</igDP:XamDataGrid>
</igDock:ContentPane>
As you can see, the only difference is highlighted above and shows the custom DataSourceProvider in action.
You can see from the image above that the designer capability is not affected with this actually loading from the database, nice!
So, what do we need to do to achieve this marvellous result. its actialy fairly simple, I got out my trusty reflector and found that there is really only one method to override.
Imports System.ComponentModel
Imports System.Threading
Public Class NinjectDataProvider
Inherits DataSourceProvider
Private m_objectType As Type
Public Property ObjectType() As Type
Get
Return Me.m_objectType
End Get
Set(ByVal value As Type)
If Not m_objectType Is value Then
m_objectType = value
Me.OnPropertyChanged("ObjectType")
If Not MyBase.IsRefreshDeferred Then
MyBase.Refresh()
End If
End If
End Set
End Property
Private Overloads Sub OnPropertyChanged(ByVal propertyName As String)
MyBase.OnPropertyChanged(New PropertyChangedEventArgs(propertyName))
End Sub
Protected Overrides Sub BeginQuery()
If m_objectType Is Nothing Then
Me.OnQueryFinished(Nothing, New InvalidOperationException("You must provide an ObjectType"), Nothing, Nothing)
End If
Dim result As Object
Try
result = Application.NinjectKernel.Get(m_objectType)
Me.OnQueryFinished(result, Nothing, Nothing, Nothing)
Catch ex As Exception
Me.OnQueryFinished(Nothing, ex, Nothing, Nothing)
End Try
End Sub
End Class
I do not yet need all the fancy features of Ninject yet so I have only implemented the bit that I need at the moment. If I am adding more (and get it working) I will blog about it in the future.
To get this working I needed to add an instance of an IKernel object to the “Application” file so I have a single Kernel instance through my application unless I want another, but this is a small price to pay and it could well have been done in the same way as the My.Unity.Resolve(Of Ninja) post I did on Unity.
Start your Ninja training today!
Friday, August 21, 2009
One of my colleagues is facing the maelstrom that is corporate blogjection and has become a geek with a blog. Have a heart as he is a poor under-paid support analyst who hits WAY above his pay grade.
Welcome Roddy… good first post on SQL Server Function to add working days on to a date, I always wanted to know how to do that!
Technorati Tags:
Blogging,
Personal
Thursday, August 20, 2009

Over the past week I have been reading the new book Silverlight 3 Programmer's Reference
from Wrox and I have found it one of the best books on Silverlight I have seen in a good while. It is concise without being boring and it provides a wealth of information on Silverlight 3.
And it is in Colour! I never would have thought that this would make such a difference, I don’t really know why I thought this as I hate looking at code in notepad, but it makes it much easier to read the code pages, both c#/vb and xaml.
Because I have been using WPF for a number of years this book is perfect for me, although this is a reference book, It has a nice layout that is conducive to both learning and reference.
Will I be hanging up my WPF hat and replacing it with a Silverlight one? Well, no… but Silverlight 3 is a big step forward…
Monday, August 17, 2009
I had previously created a Command Line Parser from Ray Hayes codeproject article Automatic Command Line Parsing in C#. I had adapted it to VB.NET and upgraded it to .NET 3.5 but I recently ran into the problem with wanting a single command prompt application to handle multiple processes and multiple parameters. This would allow you to group all of a particular tasks commands into a single application. With the advent of Power Shell this format is increasingly less relevant, but with the proliferation of Power Shell many people still prefer to use the good old command line.
So, staring from the original Command Line Parser v1.0 code I wanted to be able to add multiple commands, or even nest commands. The result is a nice simple commanding architecture conducive to creating multiple commands.

Using this model I can create a simple command…
Imports Hinshlabs.CommandLineParser
Imports System.IO
Imports System.Collections.ObjectModel
Imports System.Net
Public Class Demo1Command
Inherits CommandBase(Of Demo1CommandLine)
Private m_PortalLocation As Uri
Public Overrides ReadOnly Property Description() As String
Get
Return "demo 1 command demonstrates a sinle nested command"
End Get
End Property
Public Overrides ReadOnly Property Name() As String
Get
Return "Demo1"
End Get
End Property
Protected Overrides Function ValidateCommand() As Boolean
Return True
End Function
Public Overrides ReadOnly Property Title() As String
Get
Return "demo 1"
End Get
End Property
Public Overrides ReadOnly Property Synopsis() As String
Get
Return "demo 1 command"
End Get
End Property
Public Overrides ReadOnly Property Switches() As ReadOnlyCollection(Of SwitchInfo)
Get
Return CommandLine.Switches
End Get
End Property
Public Overrides ReadOnly Property Qualifications() As String
Get
Return String.Empty
End Get
End Property
Protected Overrides Function RunCommand() As Integer
Try
CommandOut.Warning("running Demo1")
Return -1
Catch ex As Exception
CommandOut.Error("Failed: {0}", ex.ToString)
Return -1
End Try
End Function
End Class
Or something more substantial:
Protected Overrides Function RunCommand() As Integer
Try
Dim x As New Proxies.MyApp.Configuration.ConfigurationServiceClient("BasicHttpBinding_IConfigurationService", m_PortalLocation.ToString)
x.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Delegation
Select Case CommandLine.Action
Case QuiesceAction.Offline
x.QuiesceSource(CommandLine.Source, CommandLine.Message, New TimeSpan(0))
Case QuiesceAction.Online
x.RestoreSource(CommandLine.Source)
End Select
CommandOut.Info("Source {0} has been made {1}", CommandLine.Source, CommandLine.Action.ToString)
Return 0
Catch ex As EndpointNotFoundException
CommandOut.Error("Unable to locate site. Check the value you selected for /Portal:{0}", CommandLine.Portal)
Return -1
Catch ex As Exception
CommandOut.Error("Failed: {0}", ex.ToString)
Return -1
End Try
End Function
If you are wondering where the variables come from, you can see form Demo1Command that a generic type of Demo1CommandLine is passed in. The application creates an instance of this which wraps the Ray Hayes parser to provide the values from Environment.CommandLine used on the shared methods on the CommandLineBase class.
''' <summary>
''' Created a command line object using the Environment.CommandLine information
''' </summary>
''' <typeparam name="TCommandLine">The concrete type of object to create</typeparam>
''' <returns>An instance of the object</returns>
''' <remarks></remarks>
Public Shared Function CreateCommandLine(Of TCommandLine As {New, CommandLineBase})() As TCommandLine
Return CreateCommandLine(Of TCommandLine)(Environment.CommandLine)
End Function
''' <summary>
''' Created a command line object using the Environment.CommandLine information
''' </summary>
''' <typeparam name="TCommandLine">The concrete type of object to create</typeparam>
''' <param name="CommandLine">The command line arguments to parse</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function CreateCommandLine(Of TCommandLine As {New, CommandLineBase})(ByVal CommandLine As String) As TCommandLine
Dim instance As New TCommandLine
Dim parser As New Parser(CommandLine, instance)
parser.Parse()
instance.Parser = parser
Return instance
End Function
This parser then populates the CommandLine object with values from the CommandLine passed in. For example:
Imports Hinshlabs.CommandLineParser
Imports System.Collections.ObjectModel
Public Class Demo3CommandLine
Inherits CommandLineBase
Private m_value1 As String
Private m_value2 As Value2Values = Value2Values.Value1
<CommandLineSwitch("Value1", "Adds a string value named value1"), CommandLineAlias("v1")> _
Public Property Value1() As String
Get
Return Me.m_value1
End Get
Set(ByVal value As String)
Me.m_value1 = value
End Set
End Property
<CommandLineSwitch("Value2", "Adds and enum value called value2"), CommandLineAlias("v2")> _
Public Property Value2() As Value2Values
Get
Return Me.m_value2
End Get
Set(ByVal value As Value2Values)
Me.m_value2 = value
End Set
End Property
Public Enum Value2Values
Enum1
Enum2
Enum3
End Enum
End Class
Would allow you to call [consoleApp] Demo3 /v1:”Any value you like” /Value2:Enum3 and have the correct values populated at runtime.
I have also updated with a DelegateCommand class that would allow you to call a function in the right format from anywhere:
New DelegateCommand(Of Demo3CommandLine)("Demo2", AddressOf OnDemo2Run, "demo 2", "no additional information", "demo 2 command", "This command shows how to delegate the run method using the delegate command")
The delegate command is really easy in .NET 3.5 with the only change being the addition of a variable declared as a Func in the class:
Imports Hinshlabs.CommandLineParser
Imports System.IO
Imports System.Collections.ObjectModel
Imports System.Net
Public Class DelegateCommand(Of TCommandLine As {New, CommandLineBase})
Inherits CommandBase(Of TCommandLine)
Private m_Description As String
Private m_Title As String
Private m_Synopsis As String
Private m_Qualifications As String
Private m_name As String
Private m_RunCommand As Func(Of Integer)
Public Overrides ReadOnly Property Description() As String
Get
Return m_Description
End Get
End Property
Public Overrides ReadOnly Property Name() As String
Get
Return m_name
End Get
End Property
Protected Overrides Function RunCommand() As Integer
Try
Return m_RunCommand.Invoke
Catch ex As Exception
CommandOut.Error("Failed: {0}", ex.ToString)
Return -1
End Try
End Function
Protected Overrides Function ValidateCommand() As Boolean
Return True
End Function
Public Overrides ReadOnly Property Title() As String
Get
Return m_title
End Get
End Property
Public Overrides ReadOnly Property Synopsis() As String
Get
Return Synopsis
End Get
End Property
Public Overrides ReadOnly Property Switches() As ReadOnlyCollection(Of SwitchInfo)
Get
Return CommandLine.Switches
End Get
End Property
Public Overrides ReadOnly Property Qualifications() As String
Get
Return String.Empty
End Get
End Property
Public Sub New(ByVal name As String, ByVal runCommand As Func(Of Integer), ByVal title As String, ByVal qualifications As String, ByVal synopsis As String, ByVal description As String)
m_name = name
m_RunCommand = runCommand
m_Title = title
m_Qualifications = qualifications
m_Synopsis = synopsis
m_Description = description
End Sub
End Class
If you were wondering why there are so many properties, it is to allow the help to be created automatically. For example if you call the help function on Demo3Command you will get…
With the values coming from the relevant places:
It will also support inherited CommandLine objects to minimize duplication.
I hope that if you are building command line apps that you will have a look, just remember not to spend too much effort on cmd, when Power Shell is much more suitable and accessible to non developers.
Get Command Line Parser v2.0
Friday, August 14, 2009
A colleague of mine was having a bit of trouble getting drag and drop working in a way that fitted well with the MVVM pattern. This is really quite simple once you have a certain level of understanding of Patterns, but is a complete nightmare if you do not.
One of the founding principals of MVVM is that you should never be writing code in your code behind, it should all be encapsulated away and be bindable in XAML to achieve the result. Anyone who has tackled drag and drip will have suddenly found their code behind covered in code for handling both the drag and the drop, and multiplied up when dealing with multiple controls.
I cruised the web for information, of which I found plenty and settled on an example by Bea Stollnitz of Microsoft. In her post i had found one of the best and most intuitive examples of the Drag & Drop Behaviour written in C#.
I am not going to go into all of her code which she has available for download, just to say that it is nice, and is exactly what I am looking for even with the limitations that she described.
The functionality available allows you to drag a piece of data from one ItemsControl to another of the same data type or to reorder within itself. It provides for a floating template for the dragging item and a visual cue for the drop location.
I wanted to augment this to allow for other scenarios while keeping as much functionality as possible.
Likes:
- Drag functionality
- Drag templating – nice!
- Encapsulation of logic
Dislikes:
- No way to control drop behaviour
My version lets you inject additional functionality at runtime. The adjusted class diagram shows the relationships, but we only really use the DragDropBehaviour class
You can still use the standard options:
<DockPanel>
<Label DockPanel.Dock="Top" Content="Checkout" />
<ListBox hlb:DragDropBehaviour.IsDragSource="true"
hlb:DragDropBehaviour.IsDropTarget="true"
hlb:DragDropBehaviour.DragTemplate="{StaticResource MyTemplate}"
ItemsSource="{Binding Items}"
MinWidth="100"
MinHeight="100"
AllowDrop="True"
SelectionMode="Multiple">
</ListBox>
</DockPanel>
But I have added another bindable option of DropProcessor that allows you to override the default DropProcessor to achieve whatever you want.
<ListBox hlb:DragDropBehaviour.DropProcessor="{Binding DropProcessor}"
hlb:DragDropBehaviour.IsDragSource="true"
hlb:DragDropBehaviour.IsDropTarget="true"
hlb:DragDropBehaviour.DragTemplate="{StaticResource moo}"
ItemsSource="{Binding Items}"
MinWidth="100"
MinHeight="100">
In this example I have created a little gun shop called “Nutters R’ Us” where you can buy weapons and ordinance. You can see that there is an area for weapons, and area for ordinance and an area for your selected purchases.
I have added a custom DropProcessor only to the Checkout area that only applies when you drop items of type “OrdinanceViewModel”
Public Class CheckoutDropProcessor
Inherits DropProcessor
Public Overrides Function GetDropAdorner(ByVal behaviour As DragDropBehaviour, ByVal adornerLayer As System.Windows.Documents.AdornerLayer) As DropAdorner
If TypeOf behaviour.TargetItemContainer.DataContext Is WeaponViewModel Then
If TypeOf behaviour.SourceItemContainer.DataContext Is OrdnanceViewModel Then
Return New OrdnanceToWeaponDropAdorner(behaviour, adornerLayer)
End If
End If
Return MyBase.GetDropAdorner(behaviour, adornerLayer)
End Function
Public Overrides Function IsDropAllowed(ByVal behaviour As DragDropBehaviour, ByVal draggedItem As Object) As Boolean
If Not behaviour.SourceItemContainer Is Nothing AndAlso TypeOf behaviour.SourceItemContainer.DataContext Is OrdnanceViewModel Then
If Not behaviour.TargetItemContainer Is Nothing AndAlso TypeOf behaviour.TargetItemContainer.DataContext Is WeaponViewModel Then
Return True
End If
Return False
End If
Return MyBase.IsDropAllowed(behaviour, draggedItem)
End Function
Public Overrides Sub Drop(ByVal behaviour As DragDropBehaviour, ByVal draggedItem As Object, ByVal dropEffect As System.Windows.DragDropEffects)
If Not behaviour.TargetItemContainer Is Nothing AndAlso TypeOf behaviour.TargetItemContainer.DataContext Is WeaponViewModel Then
If TypeOf behaviour.SourceItemContainer.DataContext Is OrdnanceViewModel Then
CType(behaviour.TargetItemContainer.DataContext, WeaponViewModel).AddOrdinance(CType(behaviour.SourceItemContainer.DataContext, OrdnanceViewModel))
Dim indexRemoved As Integer = -1
If ((dropEffect And DragDropEffects.Move) <> DragDropEffects.None) Then
indexRemoved = Utilities.RemoveItemFromItemsControl(behaviour.SourceItemsControl, draggedItem)
End If
If (((indexRemoved <> -1) AndAlso (behaviour.SourceItemsControl Is behaviour.TargetItemsControl)) AndAlso (indexRemoved < behaviour.InsertionIndex)) Then
behaviour.InsertionIndex -= 1
End If
Exit Sub
End If
End If
MyBase.Drop(behaviour, draggedItem, dropEffect)
End Sub
End Class
This class inherits from the base class “DropProcessor” that provides the same functionality as the original article, but I have
overridden couple of methods. The first, “GetDropAdorner” test to make sure that you are dropping a OrdinanceViewModel onto a WeaponViewModel and provides a different and custom DropAdorner that instead of providing the lovely insertion visual it just applied a “IsDropTarget” property to the ListBoxItem to allow a template to control the visual. The IsAllowedDrop also test for this case, as does the Drop method. In all cases they are just testing for a special case of Drop and call the base classes methods.
The diagram for the demo app is a little large, but you can see how much I still suck at MVVM, and although I have learned a lot doing this demo, I am still tempted to share ViewModels… but that is a bad habit.
I have highlighted the two main classes, and we have already discussed the CheckoutDropProcessor. This allows you the flexibility to augment your drop scenarios without all of your developers having to get too deep in the guts on the behaviour, thus leaving them plenty of time for the real work of actually building something useful.
I have put this up on Codeplex, and both the source and binaries are available.
Thursday, August 06, 2009
Have you been waiting for a long time for Windows 7? Well I have.. I have been able to use Beta 1 and the RC for a good while now, and it surprised me that the Windows 7 Beta 1 was more stable, responsive and cleaner than Vista was after Service Pack 3.
Today (06/08/2009) Windows 7 RTM will be available for Developers on MSDN and a lovely free copy for those that participated in the invitation only Beta program.
What a joy this day is, we finally get an OS that is both up to date and that we can use… I was nearly ready to hang up my PC and buy a MAC. Well, when I say NEARLY I really mean as far away as possible with a hazmat suit on, but you get the idea. Those of you that use Vista and like it, at least over the aging XP will love Windows 7. It is what Windows Vista should have been but wasn’t.
If you have not yet seen Windows 7 then head on over to the Windows 7 site, if you have then it will not be long until it is available. September will be the official “buy it in the shops” day, but many new PC’s already come with an automatic upgrade.
Technorati Tags:
Windows,
Windows 7
Thursday, July 30, 2009
Another nice feature of Outlook 2010 that I like is the Calendar preview:
Very effective for seeing quickly wither you can attend :)