Martin Hinshelwood

A Scottish software developer: SSW Solution Architect & Microsoft Visual Studio ALM MVP
posts - 401, comments - 554, trackbacks - 57

My Links

News

Disclaimer

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






TwitterCounter for @MrHinsh

Locations of visitors to this page

Personal

Twitter












Tag Cloud

Article Categories

Archives

Post Categories

Image Galleries

Blogs I read

Blogs of Friends

Personal

Projects

VSTS

Monday, February 08, 2010

Why I miss Orange and why Vodafone suck!

<rant>

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

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

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

That was Vodafone's second chance!

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

That was their third chance!

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

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

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

 

Technorati Tags: ,

</rant>

posted @ Monday, February 08, 2010 11:35 AM | Feedback (0) | Filed Under [ Rant ]

Saturday, January 09, 2010

Solution - IIS SMTP Service 5.5.2 rejected: need fully qualified hostname

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:

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

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

Technorati Tags: ,,,

posted @ Saturday, January 09, 2010 8:10 PM | Feedback (0) |

Monday, January 04, 2010

Solution - SEO permanent redirects for old URL’s?

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.

 

posted @ Monday, January 04, 2010 2:34 AM | Feedback (0) |

Monday, December 28, 2009

Investigation - SEO permanent redirects for old URL’s?

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

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

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


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

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

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

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

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

  • URL Rewrite
  • SEO Toolkit

URL Rewrite

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

image 
Figure: Adds an option right into the IIS interface

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

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

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

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

SEO Toolkit

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

Conclusion

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

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

posted @ Monday, December 28, 2009 2:27 PM | Feedback (0) |

Monday, December 07, 2009

Create a VHD from the Windows Server 2008 R2 Image disk

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

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

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

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

Whoa there… not so fast..

Did you spot my mistake?

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

Could it be the “CORE” bit?

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

Doh!

Lets try that properly:

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

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

posted @ Monday, December 07, 2009 2:37 PM | Feedback (0) |

Outlook 2010 Beta 2 and Add-Ins: Dynamics CRM, Team Companion, LinkedIn and Plaxo

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

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

I use a number of plug-ins for outlook:

  • LinkedIn
  • Plaxo
  • Team Companion
  • Dynamics CRM

All of them work…to an extent…

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

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

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

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

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

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

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

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

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

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

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

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

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

Please can you:

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

posted @ Monday, December 07, 2009 2:05 PM | Feedback (1) |

Create a VHD from the Windows 7 Image disk

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

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

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

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

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

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

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

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

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

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

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

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

Technorati Tags: ,,

posted @ Monday, December 07, 2009 1:51 PM | Feedback (0) |

Internet connection speed, WOW

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

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

Cable & Wireless –> NTL –> VirginMedia

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

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

Now I thought it would be fast, but OMG:

image

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

Transfer Rate: 3.201 MB/sec

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

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

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

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

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

(Update 06/12/2009)

Before upgrade (was on 20mbs):

SpeedTest.net Before (2)

After Upgrade (now on 50mbs):

644659208 

(Update 07/12/2009)

it just gets better and better:

image

Technorati Tags: ,,

posted @ Monday, December 07, 2009 12:29 AM | Feedback (1) |

Friday, December 04, 2009

Internet connection speed, WOW

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

Double posted in error please use link above...

posted @ Friday, December 04, 2009 12:50 PM | Feedback (0) |

Thursday, November 12, 2009

Installing Visual Studio 2008 Team Foundation Server SP1

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

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

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

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

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

Release Plan

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

Test Plan

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

Back out Plan

1. Restore last snapshot

2. Start TFS website in IIS

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

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

Conclusion

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

posted @ Thursday, November 12, 2009 2:54 PM | Feedback (1) |

Monday, November 02, 2009

Dyslexia Awareness Week

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

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

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

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

British Dyslexia Association: Dyslexia Awareness Week: Dyslexia Strenths

posted @ Monday, November 02, 2009 4:17 PM | Feedback (2) |

Sunday, October 25, 2009

Deploying Visual Studio 2010 Team Foundation Server Beta 2 - Done

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

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


Adam put a few guys together:

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

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

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

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

 image

Figure: Web Access – Working

VS2010

Figure: Visual Studio - Working

 

Well done to the SSW team.

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

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

 

 

       

 

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

RulestoBetter

posted @ Sunday, October 25, 2009 3:46 AM | Feedback (6) |

A change for the better #2

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

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

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

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

SSWLogo

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

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

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

#32 Do you enjoy your job?

The expectation from Adam is:

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

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

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

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

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

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

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

 

 

 

 

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

RulestoBetter

posted @ Sunday, October 25, 2009 12:39 AM | Feedback (0) |

Tuesday, October 20, 2009

Configuring Visual Studio 2010 Team Foundation Server on Vista in 12 minutes

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

clip_image001

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

Team Foundation Server Configuration

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

Team Foundation Server Configuration - Advanced

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

Team Foundation Server Configuration - Advanced - Install

Team Foundation Server Configuration - Advanced - Database

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

Team Foundation Server Configuration - Advanced - Account

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

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

Team Foundation Server Configuration - Advanced - Project Collection

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

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

 Team Foundation Server Configuration - Advanced - Rediness Checks

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

Team Foundation Server Configuration - Advanced - Configure

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

Team Foundation Server Configuration - Advanced - Configure after 20 seconds

..30 seconds...

Team Foundation Server Configuration - Advanced - Configure after 40 seconds

...50 seconds...

Team Foundation Server Configuration - Advanced - Configure after 50 seconds

.. 1 minute...

Team Foundation Server Configuration - Advanced - Configure after 60 seconds

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

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

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

... shortened ...

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

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

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

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

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

posted @ Tuesday, October 20, 2009 2:25 PM | Feedback (2) |

Installing Visual Studio 2010 Team Foundation Server on Windows Vista in 3 minutes

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

image

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

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

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

Microsoft Team Foundation Server 2010 Install

Microsoft Team Foundation Server 2010 Install - Start Page

 

Microsoft Team Foundation Server 2010 Install - Options Page

Microsoft Team Foundation Server 2010 Install - Install Page

Microsoft Team Foundation Server 2010 Install - Finish Page

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

 

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

posted @ Tuesday, October 20, 2009 1:48 PM | Feedback (7) |

Interview with Scottish Developers

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…

 

posted @ Tuesday, October 20, 2009 6:45 AM | Feedback (2) |

Monday, October 19, 2009

Visual Studio 2010 Beta 2 is available Now!

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

clip_image001

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

Visual Studio IDE now comes in these flavours:

· Microsoft® Visual Studio® 2010 Professional

· Microsoft® Visual Studio® 2010 Professional with MSDN

· Microsoft® Visual Studio® 2010 Premium with MSDN

· Microsoft® Visual Studio® 2010 Ultimate with MSDN

clip_image002

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

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

· Microsoft® Visual Studio® Test Elements 2010 with MSDN

· Microsoft® Visual Studio® Team Foundation Server 2010

· Microsoft® Visual Studio® Team Lab Management 2010

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

Check out the new channel 9 videos:

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

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

Technorati Tags: ALM,Visual Studio ALM

posted @ Monday, October 19, 2009 7:03 PM | Feedback (0) |

Monday, August 31, 2009

Wpf Scale Transform Behaviour

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

image

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

Public Class GlobalScaleTransformBehaviour

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

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

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

        Return element.GetValue(IsScaledProperty)
    End Function

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

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

End Class

 

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

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

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

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

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

Public Class GlobalScaleTransformBehaviour

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

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

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

        Return element.GetValue(IsScaledProperty)
    End Function

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

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

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

        Return element.GetValue(ScaleValueProperty)
    End Function

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

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

End Class

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

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

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

image

 

 

 

krsu46zvpt

posted @ Monday, August 31, 2009 8:48 AM | Feedback (0) |

Tuesday, August 25, 2009

Wpf Ninject Dojo: The Data Provider

You have probably heard me go on about Unity a couple of times:

I have been using what is now 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: .

I decided on my latest project (a Wpf dashboard for HEAT ITSM) that I needed dependency injection. Whenever I start building a 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 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 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 in action.

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

Start your Ninja training today!

 

posted @ Tuesday, August 25, 2009 2:43 PM | Feedback (0) |

Friday, August 21, 2009

Second blogger from my office

One of my colleagues is facing the maelstrom that is corporate blogjection and has become . 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: ,

posted @ Friday, August 21, 2009 2:46 PM | Feedback (1) |

Thursday, August 20, 2009

Silverlight 3

Over the past week I have been reading the new book Silverlight 3 Programmer's Referencefrom 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…

Technorati Tags: ,,

posted @ Thursday, August 20, 2009 7:50 PM | Feedback (1) |

Monday, August 17, 2009

Updating the Command Line Parser

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.

image

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…

image

With the values coming from the relevant places:

image

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

Technorati Tags: ,,

posted @ Monday, August 17, 2009 1:11 AM | Feedback (1) |

Friday, August 14, 2009

Wpf Drag & Drop behaviour

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 of Microsoft. In her 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.

InsertionAdorner

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

image 

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.

image

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

image

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.

 image

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.

Technorati Tags: ,,,

posted @ Friday, August 14, 2009 4:16 AM | Feedback (1) |

Thursday, August 06, 2009

The long wait is over

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.

image

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: ,

posted @ Thursday, August 06, 2009 3:40 AM | Feedback (1) |

Thursday, July 30, 2009

Finding features: Calendar preview

Another nice feature of Outlook 2010 that I like is the Calendar preview:

 image

Very effective for seeing quickly wither you can attend :)

posted @ Thursday, July 30, 2009 9:38 PM | Feedback (0) |

Powered by: