Monday, December 29, 2014

Dynamically change column width in Reporting Services 2010

I have a requirement to hide/show columns in a Table depending on the type of document being reported and adjust another column's width to compensate. Changing a column's visibility according to a formula is easy, however there is no facility to change a column's width without using code, which is still a pain in the **** in Reporting Services.

Here are the three formats I need to support...

VENDOR requires an Item Number column and a wide Description


STORES requires a Stock Number column and a wide Description

VFS requires an Item Number column and a Stock Number column and a narrow Description.

Hiding a column is easy - you right click the column's tag, click on column visibility, check the 'Show or hide based on an expression', and then enter the Hidden value. I need to hide the Stock Number column for VENDOR documents so...

But how can you change the width of a column without a Dante-like descent into code?

Fortunately Item Number and Stock Number are both the same width (I made them 0.75" each). That means the Description column needs to be 0.75" narrower when the document is VFS.

I created a new column 0.75" wide to the right of the Description column and merged the header and body cells of the Description column with the header and body cells of the new column. With the Description column's tag selected the report designer looks like this...


Then I told Reporting Services to hide the new column when the document is VFS.


Wednesday, December 3, 2014

Changing SelectedIndex on one combo box triggers SelectionChanged event on a different combo box

WPF 4.0

I just spent ten minutes trying to figure out why changing the selected index on one combo box triggers the SelectionChanged event on a different combo box.

The two combo boxes are defined thus...

<ComboBox Name="SearchTransactionTypeCombo" Binding="{StaticResource TransactionTypes}"  SelectedValuePath="GLTransactionTypeID" DisplayMemberPath="Code" Style="{StaticResource DropDownList}"></ComboBox>

<ComboBox Name="EditTransactionTypeCombo" Binding="{StaticResource TransactionTypes}"  SelectedValuePath="GLTransactionTypeID" DisplayMemberPath="Code" Style="{StaticResource DropDownList}" SelectionChanged="TransactionTypeCombo_SelectionChanged" ></ComboBox>

When I change SearchTransactionTypeCombo.SelectedIndex in code the TransactionTypeCombo_SelectionChanged event is raised and the sender object is EditTransactionTypeCombo. How can this be?

Eventually I thought to look at the DropDownList style in case I had declared an EventSetter in there. Here is the style definition...

    <Style TargetType="{x:Type ComboBox}" x:Key="DropDownList" BasedOn="{StaticResource {x:Type ComboBox}}">
        <Setter Property="IsEditable" Value="False"/>
        <Setter Property="IsSynchronizedWithCurrentItem" Value="True"/>
        <Setter Property="HorizontalAlignment" Value="Stretch"/>
    </Style>

The problem is the IsSynchronizedWithCurrentItem property. Both combo boxes are bound to the same resource. When I changed SearchTransactionTypeCombo.SelectedIndex the current row of the collection was changed, which in turn changed the selected index of EditTransactionTypeCombo which raised the TransactionTypeCombo_SelectionChanged event.

As synchronization is not really needed, I removed it from the style. Alternatively I could have bound to different resources, or overridden synchronization in the control definitions.

Monday, November 24, 2014

Automating Reporting Services Deployment

Let's assume you have reporting services installed on your production servers and you want to deploy reports as part of an automated process. You may have hundreds of production servers at various sites so you need to automate the process.

Reporting Services uses named data sources that are evaluated at run time. You can leverage this by creating a consistently named data source on each Reporting Services server that points to the SQL Server instance that holds your data. The same concept applies if your data is spread over instances. This process can be automated too, but as it only happens once per Report Server instance, I did it manually.

If, as is common, your Reporting Services server is also your SQL Server you can just have all the data sources use (local).

What changes with each new release is the datasets, the reports, and the report parts.

Devenv

If you are pushing from an environment that has Visual Studio, you can simply run devenv to deploy the report project. DevEnv can be found in Program Files\Miscrosoft Visual Studio\<version>\Common7\IDE. The following example deploys all the reports in the PurchasingReports solution using the release configuration and creates a log file in Deployment.log.

devenv.exe PurchasingReports.sln  /deploy release /out Deployment.log

You could create a different configuration for each of your Reporting Services servers and change the /deploy option to deploy to different servers. For example you could have Debug, Release, California, Ukraine, and Taiwan configurations.

But often deployment is done by pulling from the target server. This makes starting and stopping services and other scripting tasks easier. It's unlikely you will have Visual Studio installed on a production server so this devenv approach won't work.

RS.exe

RS.exe is Microsoft's weird way of allowing you to write a VB.Net script that is compiled and executed at run time in an environment that has a specific reporting services SOAP library included. The documentation says you can create the script in Visual Studio but you get no Intellisense or any other features so you may as well just write the script in Notepad. The only important thing is that the script file must have the rss extension.

Let's write a script that pulls reports and data sets from a build server, overwriting any existing copies. You can also pull data sources, security, subscriptions and a host of other things. In addition, you could do a lot of other, non-reporting, things if you wanted.

We will need a batch file to run our script. RS.exe can be found in Program Files\Microsoft SQL Server\120\Tools\Binn. I'm going to target SQL Server 2010 (see the -e option). The batch file looks like this...

rs.exe -i "rsDeploy.rss" -s (local) -e Mgmt2010

-i specifies the path to the rss script
-s specifies the name of the target Reporting Service instance
-e specifies the SOAP endpoint to target. The default is Mgmt2005.

There are other options, but they are not relevant to this post.

Now we have to write rsDeploy.rss. We could do it in Visual Studio or we could do it in Notepad. Remember it's just a console app.

We start by declaring Main(), establishing credentials, and making sure our dataset and report folders exist, Note there is an implied object called rs which is an instance of ReportingService2010 (because of the endpoint we chose). We will make heavy use of this object so it's worth looking at the documentation on MSDN. This example copies reports for my purchasing project. Substitute your own name for <build server>. 

Dim reportFolder As String = "Purchasing"
Dim reportPath as string = "/" & reportFolder
Dim reportFilePath As String = "\\<build server>\PurchasingReports"
Dim reportFileName As String = "*.rdl"
Dim datasetFolder As String = "DataSets"
Dim datasetPath as string = "/" & datasetFolder 
Dim datasetFilePath As String = "\\<build server>\PurchasingReports"
Dim datasetFileName As String = "*.rsd"
Dim definition As [Byte]() = Nothing
Dim warnings As Warning() = Nothing

Public Sub Main()

rs.Credentials = System.Net.CredentialCache.DefaultCredentials
' Create the parent folder
Try
rs.CreateFolder(reportFolder, "/", Nothing)
Console.WriteLine("Parent Folder {0} created successfully", reportFolder)
rs.CreateFolder(datasetFolder, "/", Nothing)
Console.WriteLine("Parent Folder {0} created successfully", datasetFolder)
Catch ex as Exception
Console.WriteLine("Creating Parent Folder {0}:{1}", reportFolder, ex.Message)
End Try

End Sub

Next we will loop through our datasets and deploy them one by one. Add the following code to the end of Main.

' Publish the datasets
Dim datasets as string() = System.IO.Directory.GetFiles(datasetFilePath, datasetFileName)
For Each dataset as string in datasets
PublishDataSet(dataset)
Next

The PublishDataSet method is quite simple. A lot of information is returned in the warnings collection and in the CI object.

Public Sub PublishDataSet(dataset as string)

Dim stream As FileStream
Dim datasetName As String
Dim CI as CatalogItem

Try
Console.Write("Publishing dataset {0}:", dataset)
stream = File.OpenRead(dataset)
definition = New [Byte](stream.length - 1){}
stream.Read(definition, 0, CInt(stream.length))
stream.Close()
datasetName = System.IO.Path.GetFileName(dataset).replace(".rsd","")
CI = rs.CreateCatalogItem("DataSet", datasetName, datasetPath, true, definition, Nothing, warnings)

Console.WriteLine("OK")
Catch ex as Exception
Console.WriteLine(ex.Message)
End Try
End Sub

Publish reports is almost identical. You can see how you could extend this to deploy other reporting services features. Add the following to Main...

' Publish the reports
Dim reports as String() = System.IO.Directory.GetFiles(reportFilePath, reportFileName)
For Each report as String in reports
PublishReport(report)
Next

and add this method too. Note that after deploying the report we need to link it to the consistently named data source I mentioned at the top of the article.

Public Sub PublishReport(report as String)
Dim stream As FileStream
Dim reportName As String
Dim CI as CatalogItem
Try
Console.Write("Publishing {0}:", report)
stream = File.OpenRead(report)
definition = New [Byte](stream.length - 1){}
stream.Read(definition, 0, CInt(stream.length))
stream.Close()

reportName = System.IO.Path.GetFileName(report).replace(".rdl","")
CI = rs.CreateCatalogItem("Report", reportName, reportPath, true, definition, Nothing, warnings)
' Set the DataSource
Dim ds(0) as DataSource
Dim s as new DataSource
Dim dsr As New DataSourceReference
dsr.Reference = "/Data Sources/Purchasing"
s.Item = dsr
s.Name="Purchasing"
ds(0) = s
Dim myItem As String = reportPath & "/" & reportName
rs.SetItemDataSources(myItem, ds)
Console.WriteLine("OK")
Catch ex as Exception
Console.WriteLine(ex.Message)
End Try
End Sub

Now all we have to do is run the batch file, fix all the build errors and we're good to go.

Sunday, October 26, 2014

Cannot access Properties.Settings from App.xaml.vb

I don't generally like the Settings class in the .Net framework because it does not work well with ClickOnce deployment. If you alter the settings collection between deployments, the new deployment thinks the users' settings files are corrupt and refuses to run until you destroy them. This pretty much renders settings useless.

However I noticed another problem the other day while messing around. Let's pretend your project has a setting of "Name" defined as a string with user scope. To access this setting in the main window you would use...

String Name;
Name = Properties.Settings.Default.Name;

Now try the same thing in App.xaml.vb and the compiler will complain that Settings is not valid.


The clue is buried in the error message. It's expecting a dictionary. What's happening here is that the compiler is confusing Properties.Settings with Application.Current.Properties(...) and it's expecting a dictionary key (or some dictionary reference) to follow "Properties".

To reference the settings from the App.xaml.vb you need to explicitly prefix "Properties" with the default namespace ie.

String Name;
Name = Lesson_2.Properties.Settings.Default.Name;

The best approach would be to always use the fully qualified reference for both Properties.Settings and Application.Current.Properties ie.

String Name;
Name = Lesson_2.Properties.Settings.Default.Name;
Application.Current.Properties["Name"] = Name;

Of course, Visual Basic uses the My syntax to access settings which avoids this confusion completely.


Tuesday, October 21, 2014

Reporting Services generates invalid pdf files

Let's be honest here - the fault was all mine although it took a long time to realize why.

This isn't really a WPF issue at all, but it is a WCF issue and I'm only using WCF because the client is in WPF. The question is "How do I tell Reporting Services to render a report and return the pdf to me so I can send it back to the client?" I have a WCF service doing this so I can take more control over parameters, security, etc.

There's dozens of ways to do this and I explored most of them. The one that made most sense to me was to create a web request object and grab the response in a web response. Then I save the ResponseStream to a temporary file which gives me the option to dynamically stitch a bunch of reports together using ExpertPDF and return a single pdf file to the client as a byte array.

You have to be very careful to use the correct methods and understand how they work. For example, System.IO.File.ReadToEnd returns a string which can cause pdf files (which can be binary) to be corrupted. Also Stream.Read does not necessarily read all the bytes you asked for. You have to keep calling it until the stream is fully read.

If you only execute a single call to Stream.Read you may not completely populate the byte array - and your pdf file will be corrupted.

Here's the code to execute a report and save it to a temporary file...

    Public Function RunReportingServicesReport() As String

        Dim URL As String
        Dim Request As System.Net.HttpWebRequest = Nothing
        Dim Response As System.Net.HttpWebResponse = Nothing
        Dim ReportName As String = Nothing
        Dim Stream As System.IO.Stream = Nothing
        Dim Bytes As Byte()
        Dim BytesRead As Integer = 0
        Dim BytesToRead As Integer
        Dim Offset As Integer = 0
        Try
            URL = "http://ReportServer/ReportService?/Reports/MyReport&rs:Format=PDF&rs:Command=Render"
            Request = System.Net.WebRequest.Create(URL)
            Request.PreAuthenticate = True
            Request.Credentials = New System.Net.NetworkCredential("User", "Password", "Domain")

            Response = Request.GetResponse()
            ReportName = System.IO.Path.GetTempFileName()
            Stream = Response.GetResponseStream()
            BytesToRead = Response.ContentLength
            Bytes = New Byte(BytesToRead - 1) {}
            Do While BytesToRead > 0
                BytesRead = Stream.Read(Bytes, Offset, BytesToRead)
                Offset += BytesRead
                BytesToRead -= BytesRead
            Loop
            System.IO.File.WriteAllBytes(ReportName, Bytes)

        Catch ex As System.Exception
            Throw New System.Exception("RunReportingServicesReport:" & ex.Message)
        Finally
            If Stream IsNot Nothing Then Stream.Close()
            If Response IsNot Nothing Then Response.Close()
        End Try

        Return ReportName

    End Function

Monday, September 29, 2014

Poor man's compiler error list

Every now and again I will have an error in my WPF application (no, really, it does happen!) and the compiler gets its panties in a bind. I'll get a shit load of errors from reference.vb and the generated files and hit the max error limit without any real errors actually being displayed. There is no way to override the error limit so how is a developer supposed to fix the error if the compiler won't show it?

Well there is a way.

Look at the Output window and scroll until you see an error message something like this.




You can even double click on it to jump to the line of code that contains the error. The line is highlighted with a blue dash to the left and the text cursor is placed at the point the compiler thinks contains the error.



Thursday, September 25, 2014

Automating ClickOnce deployment and WCF hosting

I'm at the point where I need to deploy my new WPF application and it's WCF services as part of an automated build. This involves learning a little about MSBuild, a complex application that Microsoft has somehow failed to document in any meaningful way. You can find it in the framework folder (Windows\Microsoft.Net\Framework\v<frameworkversion>\)

MSBuild is used by Visual Studio to perform build and publish functions but can be run from the command line. It uses the project file (vbproj or csproj) together with some parameters from the command line to build and/or publish specific configurations. For example...

msbuild Purchasing.vbproj /t:Publish /p:Configuration=Release /p:PublishDir="\\<production server>\Purchasing\PurchasingClient\\" /v:normal > Log.txt

This command instructs msbuild to perform a release build (using the PropertyGroup with a configuration parameter = Release) and then publish the application to the specified shared folder on the production server. The output is written to a log file. I could not get it to publish to a URL, but that might just be a matter of configuring publish to URL in visual studio, looking at the project file, and looking for the PublishURL task. Then you could override it with a /p:PublishURL parameter.

Note the double slash at the end of PurchasingClient\\. This gets around an odd bug. It seems as though a single slash following by a quote is being interpreted as an attempt to embed the quote into the parameter value. A single slash throws an invalid character error. A double slash does what we want.

The big problem is that MSBuild cannot increment the publish version which upsets the clients when they try to download the application in online-only mode. The client reports an error saying that there is already an application with the same version number installed. Although Visual Studio can auto increment publish version, this is not done via MSBuild so we need to find another way.

All we need to do is increment the content of an element with an XPath of \Project\PropertyGroup\ApplicationRevision. There are several ways we can do this. In the end I found it easier to write a very simple app that takes an XML file name and one or more XPaths. If the content of the elements matching those XPaths is numeric, I increment it and save the file. This is only needed for the WPF applications. I called the application ProjectVersionIncrementer. There's probably a better way to handle the namespace issue, but I was in a hurry.

Module ProjectVendorIncrementer Sub Main(Args() As String) Dim XMLFileName As String Dim XMLFile As New System.Xml.XmlDocument Dim XPath As String Dim sValue As String Dim IsIncremented As Boolean = False Dim mgr As System.Xml.XmlNamespaceManager Try XMLFileName = Args(0) XMLFile.Load(XMLFileName) mgr = New System.Xml.XmlNamespaceManager(XMLFile.NameTable) mgr.AddNamespace("m", XMLFile.DocumentElement.NamespaceURI) Console.WriteLine("Loaded " & XMLFileName) For i As Integer = 1 To Args.Count - 1 Console.WriteLine("Searching for XPath " & Args(i)) XPath = Args(i).Replace("/", "/m:").Replace("/m:/m:", "//m:") For Each XMLNode As System.Xml.XmlNode In XMLFile.DocumentElement.SelectNodes(XPath, mgr) sValue = XMLNode.InnerText If IsNumeric(sValue) Then sValue = (CDec(sValue) + 1).ToString() Console.WriteLine(" Incremented " & XMLNode.Name & " to " & sValue) XMLNode.InnerText = sValue IsIncremented = True End If Next Next If IsIncremented Then XMLFile.Save(XMLFileName) Console.WriteLine("Saved Changes") End If Catch ex As Exception Console.WriteLine("ERROR:" & ex.Message) Finally Console.WriteLine("Done") End Try End Sub End Module

So to increment the version numbers of all configurations of a local file called Purchasing.vbproj you would add the following line at the beginning of your publish bat file.

ProjectVersionIncrementer.exe Purchasing.vbproj //Project/PropertyGroup/ApplicationRevision

Follow this with the msbuild line from the top of the blog post and you've got a bat file that will increment the publish number and publish to the server of your choice.

Automating the publication of WCF services is similar, but different. You don't have to worry about versioning because you're not using ClickOnce. On the other hand msbuild won't handle the deployment. There's a utility called MSDeploy that's supposed to do that but it kept complaining about ACLs so, as it's basically just copying files to a known location I used xcopy instead.

The bat file looks like this and is run from the vbproj folder,,,

C:\Windows\Microsoft.Net\Framework\v4.0.30319\msbuild.exe WCFDatabase.vbproj /t:Package /p:Configuration=Release /v:normal > Log.txt

xcopy obj\Release\Package\PackageTmp \\<Production Server>\Purchasing\WCFDatabase /i /s /y

Although our development boxes are all running Windows 7, our test and production servers are still on Windows Server 2003 SP 2 which only runs IIS 6.0 and framework 4.0. Configuring IIS 6.0 to host WCF services requires some manual labor.


  1. Register the framework by browsing to the framework version and running aspnet_regiis.exe /i /enable
  2. Find ServiceModelReg.exe and run it with /r /x /v /y
  3. In IIS management studio create a new virtual directory that points to the directory you copied the WCF service svc file to. Make sure the virtual directory is configured for the correct version of the framework.
  4. Restart IIS using iisreset.exe
I created a single shared directory under Inetpub/wwwroot/Purchasing and then a new subfolder to host each WCF service. The xcopy for the service copies the files via the shared directory to the subfolder. I also created a new web application for each service that points to the service's subfolder. The web applications are configured for read/write/execute. Each one must be in a classic app pool.

If, after doing this, you still get 405 Not Allowed errors it might mean your .svc extensions didn't get registered, Don't panic - it's not difficult but it is tedious.

Right-click on each of the WCF service virtual directories and select Properties. Then click on the [Configuration...] button. If your Application Extensions list does not include .svc you will need to add it. Click [Add...]

Type "c:\windows\microsoft.net\framework\v4.0.30319\aspnet_isapi.dll" into the Executable: textbox and ".svc" into the Extension: textbox. Leave the defaults in place. Be sure to use the correct version of the framework in the executable path. Repeat for each WCF service.

Monday, September 22, 2014

DatePicker won't update source field

This really has nothing to do with date picker - it has to do with a retarded programmer - namely me.

I have a date picker (could be any control) with a selected date bound to a particular column in a particular datarow. I also have the following piece of code to initialize the date if it is empty in the datarow.

If ToDatePicker.SelectedDate is nothing then ToDatePicker.SelectedDate = today

That seems OK. It works, it displays today's date if the database field is DBNull. The problem is that when the user selects a different date, the source field is not updated.

Of course the smarter programmers have already seen the problem. I broke the binding so the DatePicker is no longer bound to the datarow. The correct solution is...

If Convert.IsDBNull(DR("to_date")) then DR("to_date") = today

This has to be one of the most difficult paradigm shifts required to be a good WPF programmer. It certainly has proved to be that way for me.

Saturday, September 13, 2014

WPF Unit Testing without Automation

Here's a simpler approach to WPF unit testing that doesn't use the complex and long-winded Automation classes. I simply modify the element properties directly and use RaiseEvent to simulate button presses. Then I look at the results to determine if the test worked or not.

In this example I created a window that will accept two numbers and add them together. I test that the numbers got added correctly and also that error handling correctly detected invalid numbers.

Let's start with the main window, it ain't pretty ...

<Window x:Class="Automation.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <StackPanel Orientation="Horizontal" Height="20"> <TextBox Name="Operand1TextBox" Width="50"/> <TextBlock Text=" + "/> <TextBox Name="Operand2TextBox" Width="50"/> <TextBlock Text=" = "/> <TextBlock Name="ResultTextBlock" Width="50"/> <Button Name="ComputeButton" Content="Compute" Click="ComputeButton_Click"/> <TextBlock Name="ErrorTextBlock" Foreground="Red" Width="200"/> <Button Name="TestButton" Content="Test" Click="TestButton_Click"/> </StackPanel> </Window>
----------------------------------------------------------------------------------------
using System.Windows; namespace Automation { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void ComputeButton_Click(object sender, RoutedEventArgs e) { decimal Dec1 = 0; decimal Dec2 = 0; decimal Result = 0; ResultTextBlock.Text = ""; ErrorTextBlock.Text = ""; if (!(decimal.TryParse(Operand1TextBox.Text, out Dec1))) { ErrorTextBlock.Text = "Operand 1 is not a number"; return; } if (!(decimal.TryParse(Operand2TextBox.Text, out Dec2))) { ErrorTextBlock.Text = "Operand 2 is not a number"; return; } Result = Dec1 + Dec2; // Change this line when you want to break functionality ResultTextBlock.Text = Result.ToString(); } private void TestButton_Click(object sender, RoutedEventArgs e) { Window oTest = new AutomationTests(); oTest.Owner = this; oTest.ShowDialog(); } } }
The [Test] button will popup the AutomationTests window as a dialog with the MainWindow as it's owner. That allows the test window to get easy access to the window it is testing. Here's the XAML and code for the test window, it's even uglier...

<Window x:Class="Automation.AutomationTests" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="AutomationTests" Height="300" Width="300"> <StackPanel Orientation="Vertical"> <Button Name="IntegerTest" Click="IntegerTest_Click" Content="Integer Test"/> <Button Name="DecimalTest" Click="DecimalTest_Click" Content="Decimal Test"/> <Button Name="Error1Test" Click="Error1Test_Click" Content="Error1 Test"/> <Button Name="Error2TTest" Click="Error2Test_Click" Content="Error2 Test"/> <TextBlock Name="ResultTextBlock"/> </StackPanel> </Window>
-----------------------------------------------------------------------------------------
using System.Windows; using System.Windows.Controls; namespace Automation { public partial class AutomationTests : Window { public AutomationTests() { InitializeComponent(); } private void IntegerTest_Click(object sender, RoutedEventArgs e) { MainWindow oMain = (MainWindow)Owner; oMain.Operand1TextBox.Text = "100"; oMain.Operand2TextBox.Text = "200"; oMain.ComputeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oMain.ComputeButton)); if (oMain.ResultTextBlock.Text == "300") ResultTextBlock.Text = "100 + 200 = 300 (Correct)"; else ResultTextBlock.Text = "Incorrect result, expected 300"; } private void DecimalTest_Click(object sender, RoutedEventArgs e) { MainWindow oMain = (MainWindow)Owner; oMain.Operand1TextBox.Text = "10.5"; oMain.Operand2TextBox.Text = "12.4"; oMain.ComputeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oMain.ComputeButton)); if (oMain.ResultTextBlock.Text == "22.9") ResultTextBlock.Text = "10.5 + 12.4 = 22.9 (Correct)"; else ResultTextBlock.Text = "Incorrect result, expected 22.9"; } private void Error1Test_Click(object sender, RoutedEventArgs e) { MainWindow oMain = (MainWindow)Owner; oMain.Operand1TextBox.Text = "A"; oMain.Operand2TextBox.Text = "200"; oMain.ComputeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oMain.ComputeButton)); if (oMain.ResultTextBlock.Text == "") ResultTextBlock.Text = "A + 200 = Error (Correct)"; else ResultTextBlock.Text = "Incorrect result, expected error"; } private void Error2Test_Click(object sender, RoutedEventArgs e) { MainWindow oMain = (MainWindow)Owner; oMain.Operand1TextBox.Text = "100"; oMain.Operand2TextBox.Text = "B"; oMain.ComputeButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, oMain.ComputeButton)); if (oMain.ResultTextBlock.Text == "") ResultTextBlock.Text = "100 + B = Error (Correct)"; else ResultTextBlock.Text = "Incorrect result, expected error"; } } }
As you can see, each of the four buttons on the test window manipulates the data on the main window and then clicks the [Compute] button and tests the result. It's very easy to set up test scenarios that have predictable results.

Click on each of the four test buttons. You can see that the results are correct.




Now, in the main window, replace the + with a * so that the results will be product instead of the sum. This simulates a developer breaking functionality. Perform the tests again. You can see the first two tests fail, but the second two test still work correctly.




WPF Automation

I've been working through the 70-511 test preparation book, which is a terrible book but the only one focused on this certificate. I got to the section on automation and I just don't get it. The idea is to build unit tests for applications that replicate the user performing a well defined series of tasks.

Here is an example of using automation to simulate a button click pulled straight from the book...

aWindow = New MainWindow aWindow.Show() Dim BAutomationPeer As New Automation.Peers.ButtonAutomationPeer(aWindow.Button1) Dim InvProvider As IInvokeProvider InvProvider = BAutomationPeer.GetPattern(PatternInterface.Invoke) InvProvider.Invoke()

Four lines of code to simulate a click? Why is that better than...

aWindow = New MainWindow aWindow.Show() aWindow.Button1.RaiseEvent(New RoutedEventArgs(Button.ClickEvent, Button1))

I'm pretty sure I can simulate any event I want to with the RaiseEvent method.

Also the book example creates the window to be tested from the testing window. This has got to be a bad pattern. Surely the testing window should find an already instantiated window and obtain a reference to it. Perhaps something like...

Dim aWindow as MainWindow = Application.Current.MainWindow aWindow.Button1.RaiseEvent(New RoutedEventArgs(Button.ClickEvent, Button1))
I rather like the idea of having an icon on a page that has test scripts (hidden to regular users) that opens a page of test scripts and passes the page as a reference to the test page. Then the tester could easily perform standard tests. The scripts should also include tests of the results too. For example a script that tests the creation of a balanced purchase order would test the net amount is zero after the purchase order is entered.

What is it about Automation I am missing? The book doesn't explain.

Saturday, August 9, 2014

70-502 Microsoft Certification

Now that I have almost completed the Microsoft 70-502 WPF certification book I started trying to schedule the exam at Prometric but 70-502 is not in their list of exams. This was a bit disconcerting considering that the top Google result for "70-502 Certification" is a Microsoft page that in no way suggests this certification has been retired.

After much research I discovered that the 70-502 certificate has been replaced by MCTS (Exam 70-511): Windows Applications Development with Microsoft .NET Framework 4. This is made even more confusing by the fact that the terms "WPF" and "Windows Presentation Foundation" are conspicuously missing from the title of the certification.

I guess their attitude is they don't have to try hard. Accommodating their incompetence is just part of what it takes to get certified.

I ordered the new book from Amazon. I hope they don't retire this certificate before I'm done reading it.

Friday, August 1, 2014

Changing a binding in code

If you have ever tried to modify a binding in code like this DetailsQty.Binding.StringFormat = "0" you have probably seen the following error...

Binding cannot be changed after it has been used

Previously I discovered this problem with styles and the solution there is to create a new style based on the original one, modify it, then assign it back to the UIElement replacing the original one. Not elegant, but it works.

Dim Style As System.Windows.Style Style = New System.Windows.Style(GetType(TextBox), DetailsQty.EditingElementStyle) Style.Setters.Add(New Setter(TextBox.TagProperty, "8N")) DetailsQty.EditingElementStyle = Style
Unfortunately there is no Binding constructor that takes an existing binding object so we are going to have to work harder. The Binding class does have a Clone method but it is private and we would have to use reflection to call it. I don't want to do that. I wrote a Clone method for bindings. It's very simple. The only tricky bit is that Source, RelativeSource and ElementName are mutually exclusive so I only copy them if they have values.
Public Shared Function CloneBinding(OldBinding As Binding) As Binding Dim NewBinding = New Binding If OldBinding.Source IsNot Nothing Then NewBinding.Source = OldBinding.Source NewBinding.AsyncState = OldBinding.AsyncState NewBinding.BindingGroupName = OldBinding.BindingGroupName NewBinding.BindsDirectlyToSource = OldBinding.BindsDirectlyToSource NewBinding.Converter = OldBinding.Converter NewBinding.ConverterCulture = OldBinding.ConverterCulture NewBinding.ConverterParameter = OldBinding.ConverterParameter If OldBinding.ElementName IsNot Nothing Then NewBinding.ElementName = OldBinding.ElementName NewBinding.FallbackValue = OldBinding.FallbackValue NewBinding.IsAsync = OldBinding.IsAsync NewBinding.Mode = OldBinding.Mode NewBinding.NotifyOnSourceUpdated = OldBinding.NotifyOnSourceUpdated NewBinding.NotifyOnTargetUpdated = OldBinding.NotifyOnTargetUpdated NewBinding.NotifyOnValidationError = OldBinding.NotifyOnValidationError NewBinding.Path = OldBinding.Path If OldBinding.RelativeSource IsNot Nothing Then NewBinding.RelativeSource = OldBinding.RelativeSource NewBinding.StringFormat = OldBinding.StringFormat NewBinding.TargetNullValue = OldBinding.TargetNullValue NewBinding.UpdateSourceExceptionFilter = OldBinding.UpdateSourceExceptionFilter NewBinding.UpdateSourceTrigger = OldBinding.UpdateSourceTrigger NewBinding.ValidatesOnDataErrors = OldBinding.ValidatesOnDataErrors NewBinding.ValidatesOnExceptions = OldBinding.ValidatesOnExceptions NewBinding.XPath = OldBinding.XPath Return NewBinding End Function
Now changing an active binding in code is much easier. To change the FormatString on a DataGridTextColumn called DetailsQty we would use this code...
Dim Binding As Binding Binding = CloneBinding(DetailsQty.Binding) Binding.StringFormat = "0" DetailsQty.Binding = Binding

Saturday, July 26, 2014

More about Validation

WPF 4.0 has some powerful validation features which are readily declared in XAML. I have put together a small project that demonstrates some validation techniques and also highlights a curious issue.

Let's start by defining a very simple window with one text box that only accepts letters. If the user enters a non-letter the textbox validation will trigger an error which will cause the textbox to get a red outline. We will write a custom validator to do this.

Start a new WPF Application project in C# and call it "SimpleValidator". Change the XAML and code behind to look like this...

<Window x:Class="SimpleValidator.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:SimpleValidator" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <local:User x:Key="theUser"/> </Window.Resources> <StackPanel Orientation="Horizontal" Height="30"> <Label>First Name:</Label> <TextBox Name="NameTextBox" Width="200"> <TextBox.Text> <Binding Source="{StaticResource theUser}" Path="Name" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <local:OnlyLetterValidation/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </StackPanel> </Window> ---------------------------------------------------------------------------------------------- using System; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; namespace SimpleValidator {
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } public class User { public string Name { get; set; } } public class OnlyLetterValidation : ValidationRule { public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { if (value.ToString().All(char.IsLetter)) return new ValidationResult(true, ""); else return new ValidationResult(false, "Only letters allowed"); } } }
Note the use of the String.All method (a linq extension). This is a cool technique for testing every character in a string. Also check out the Any method.

If you run this project you will see that as soon as you enter a non-letter the textbox border goes red to indicate that it has failed validation. If you put a breakpoint on the OldLetterValidation.Validate method you will see it is called whenever the user changes the value in the textbox. This is because UpdateSourceTrigger is set to "PropertyChanged" in the XAML. Validation occurs when the Source is updated. If you want to only validate when the user exits the field you should set UpdateSourceTrigger to "LostFocus".

At the moment we cannot tell the user why the textbox failed validation. There are at least three ways to do this.
  1. Bind a control to the Textbox's Validation.Errors collection. This is an attached property which exposes a collection of ValidationError objects. You could attach a list box's ItemsSource to the collection or attach a tooltip or textblock to it using a converter to convert the collection to a string.
  2. Mark the Textbox to raise Validation events when validation errors are added or removed and consume those events.
  3. Create a custom ErrorTemplate for the textbox. This is what we are going to do now.
Insert the following XAML after the </TextBox.Text> tag. Note the AdornedElementPlaceholder simply creates a placeholder for the UIElement that is being templated.

<Validation.ErrorTemplate> <ControlTemplate> <StackPanel Orientation="Horizontal"> <AdornedElementPlaceholder/> <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/> </StackPanel> </ControlTemplate> </Validation.ErrorTemplate>

Now run the application again and type a non-letter into the textbox. See how the error message is now displayed to the right of the text box.

Now let's add another validation rule. This will be a minimum length rule and will demonstrate how to create a validation with a parameter and how multiple validations work.

Start by adding a second validation class to the code behind. Add this underneath the existing OnlyLettersValidation class.

public class MinLengthValidation : ValidationRule { public int MinLength { get; set; } public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { if (value.ToString().Length < MinLength) return new ValidationResult(false, string.Format("At least {0} letters are required", MinLength)); else return new ValidationResult(true, ""); } }

This class exposes a public property which means the XAML can easily access that property, thus allowing parameters to be passed to validators. Note this is not a DependencyProperty so you cannot bind to it in XAML, although it can be done with more work. This is outside the scope of this blog entry.

Using the validator in XAML couldn't be much easier. Just add the following line at the top of the list of ValidationRules.

<Binding.ValidationRules> <local:MinLengthValidation MinLength="5"/> <local:OnlyLetterValidation/> </Binding.ValidationRules>

Run the project again and you will see that as soon as you start typing you see the minimum length error displayed. It only goes away when you have entered at least five letters.

But how do we tell the user when they first see the text box that it requires at least five characters. It should start in an error state because it is empty. We fix this with one line of code that explicitly initializes the text property of the textbox. Because we have two-way binding this causes the source to be updated. Validation occurs whenever the source is updated. Add the following line of code after the InitializeComponents line in the window's constructor.

public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); NameTextBox.Text = ""; } }

Run the project now and you will see the text box get validated before the page displays.

Now we have two validators. If you recall the textbox has an attached property called Validation.Errors which exposes a collection of ValidationError objects. However, if you put a breakpoint on each of our validators and type a value that fails both rules ie a space, you will see that validation stops at the first error. I cannot actually find a way to get more than one validation error in the Validation.Errors collection. Oh well!

There's one more thing I want to demonstrate in this blog entry. Sometimes we would like to change the color of another field, normally the prompt field, when validation fails. We can do this with binding although we do need a converter to convert from a boolean to a brush.

Let's start by adding a simply converter to our code behind.

public class ValidationConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { if (value is Boolean) { if ((Boolean)value) return new SolidColorBrush(Colors.Red); else return new SolidColorBrush(Colors.Black); } return DependencyProperty.UnsetValue; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }

Now we need to add a reference to the converter in the Windows.Resources section like this.

<Window.Resources> <local:User x:Key="theUser"/> <local:ValidationConverter x:Key="ValidationConverter"/> </Window.Resources>

And finally we bind the foreground property of our label using this rather intimidating binding clause. Notice the Path attribute has parentheses around it because Validation is an attached property.

<Label Grid.Row="0" Grid.Column="0" Foreground="{Binding ElementName=NameTextBox, Path=(Validation.HasError), Converter={StaticResource ValidationConverter}}">First Name: </Label>

Now run the project one more time and you will see the foreground color of the label turns red whenever the textbox fails validation. You could just have easily have changed the tooltip using a converter that grabbed the text of the first validation error, etc.

Wednesday, July 23, 2014

Bug when styling DatagridCheckBoxColumn

There is a bug in WPF 4.0 that breaks the IsReadOnly property of the DataGridCheckBoxColumn when it is styled. To see the problem, create a new WPF project with the following XAML and code behind.
<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}"> <Window.Resources> <Style x:Key="CheckBox" TargetType="{x:Type CheckBox}"> <Setter Property="Background" Value="Aqua"/> </Style> </Window.Resources> <StackPanel Orientation="Vertical"> <CheckBox Name="DisableGridCheckBox" Content="Disable Grid"/> <DataGrid Name="DataGrid" AutoGenerateColumns="False" ItemsSource="{Binding DataSource}" IsReadOnly="{Binding ElementName=DisableGridCheckBox, Path=IsChecked}"> <DataGrid.Columns> <DataGridTextColumn x:Name="TextColumn" Header="Text" Binding="{Binding TextField}"/> <DataGridCheckBoxColumn x:Name="UnstyledCheckboxColumn" Header="Unstyled" Binding="{Binding CheckBox1}"/> <DataGridCheckBoxColumn x:Name="StyledCheckboxColumn" Header="Styled" Binding="{Binding CheckBox2}" ElementStyle="{StaticResource CheckBox}"/> </DataGrid.Columns> </DataGrid> </StackPanel> </Window> --------------------------------------------------------------------------- Imports System.Data Class MainWindow Public Property DataSource As New DataTable Public Sub New() DataSource.Columns.Add(New DataColumn("TextField", GetType(String))) DataSource.Columns.Add(New DataColumn("CheckBox1", GetType(Boolean))) DataSource.Columns.Add(New DataColumn("CheckBox2", GetType(Boolean))) DataSource.Rows.Add("Text", True, False) InitializeComponent() End Sub End Class
When you run this project you will see an editable grid with a text box, an unstyled checkbox and a styled checkbox (with a pretty aqua background). Modify the text and check the checkboxes to verify that they have inherited the grid's IsReadOnly property.

Now check the top checkbox labeled "Disable Grid". This will put the grid into ReadOnly mode. Verify you cannot change the text or check the unstyled checkbox. However you can still check the styled checkbox. This is a bug.

Styling the checkbox has broken the IsHitTestVisible property (and others too, such as the Horizontal Alignment). To fix this, you need to alter the style to bind the IsHitTestVisible property to the parent datagrid's IsReadOnly property. The easiest way to do this is with a DataTrigger. Add the following to the style. Note that when the parent DataGrid.IsReadOnly is true we want the Checkbox.IsHitTestVisible to be false.

<Setter Property="IsHitTestVisible" Value="True"/> <Style.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=DataGrid}, Path=IsReadOnly}" Value="True"> <Setter Property="IsHitTestVisible" Value="False"/> </DataTrigger> </Style.Triggers>

Wednesday, July 16, 2014

Subtle effect of UpdateSourceTrigger

I'm writing my own numeric edit box by styling a standard textbox. Everything was going well until I notices that removing the first zero from "0.00" was causing problems. On the screen the textbox showed ".00" but the textbox text property during the PreviewKeyDown event was still "0.00" (that's in the keydown event after the one that deleted the zero). After a lot of digging I realized the problem was that the textbox had UpdateSourceTrigger=PropertyChanged.

Try out the following XAML and VB to see the problem. Run the project with the Output window visible. Put the cursor after the first zero in the top textbox and press backspace, then cursor right. The textbox will show ".00" but the TextBox.Text property will be "0.00". Interestingly the TextBox.GetLineText(0) method returns the correct value.

Now do the same thing with the bottom TextBox and you will see the TextBox.text property now shows the correct value. The only difference is the UpdateSourceTrigger value.


<Window x:Class="MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="30"></RowDefinition> <RowDefinition Height="30"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="120"></ColumnDefinition> <ColumnDefinition Width="120"></ColumnDefinition> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0" Content="PropertyChanged"></Label> <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=Decimal0, Mode=TwoWay, StringFormat={}{0:0.00}, UpdateSourceTrigger=PropertyChanged}" PreviewKeyDown="BoundTextBox_KeyDown"></TextBox> <Label Grid.Row="1" Grid.Column="0" Content="LostFocus"></Label> <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Path=Decimal1, Mode=TwoWay, StringFormat={}{0:0.00}, UpdateSourceTrigger=LostFocus}" PreviewKeyDown="BoundTextBox_KeyDown"></TextBox> </Grid> </Window> Class MainWindow Public Property Decimal0 As Decimal = 0 Public Property Decimal1 As Decimal = 0 Private Sub BoundTextBox_KeyDown(sender As System.Object, e As System.Windows.Input.KeyEventArgs) Dim oTextBox As TextBox = DirectCast(e.OriginalSource, TextBox) Trace.WriteLine("Text=[" & oTextBox.Text & "], Line(0)=[" & oTextBox.GetLineText(0) & "]") End Sub End Class
The problem is that the Textbox is attempting to update the backing store Decimal value every time the value changes. When the content of the textbox is a valid decimal it gets stored and reformatted so that the Text property does not match what's visible on the screen.

Surely this is a bug.

This behavior can be altered by either setting UpdateSourceTrigger=LostFocus (or Explicit) or setting Mode=OneWay.

Thursday, July 10, 2014

Problem when two-way binding radio buttons using a converter

In an earlier post I showed how to use a converter to bind radio buttons but I made a subtle error in the ConvertBack code that caused havoc.

Here is the ConvertBack method as I originally wrote it.

Public Function ConvertBack(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack ' Return the parameter if the button is checked If value Is Nothing Or parameter Is Nothing Then Return Nothing End If Dim IsChecked As Boolean = False Boolean.TryParse(value, IsChecked) If IsChecked Then Return parameter Else Return Nothing End If End Function
The symptoms were that occasionally the source of the binding was being set to DBNull and saved to the database. It should always be "1" or "2".

After much head-scratching and cursing of Microsoft I noticed the problem occurred when I checked the first radio button in the group.

This causes the ConvertBack method to be called for the first radio button with a parameter of "1". Because the radio button is checked the ConvertBack method returns "1" and the source is set to "1".

Then the ConvertBack method is called for the second radio button with a parameter of "2". Because the second radio button is not checked the ConvertBack method returns nothing which causes the source to be set to DBNull.

The solution is to change both "Return Nothing" lines to "Return DependencyProperty.UnsetValue". This tells the framework not to change the source value, which leaves it as "1".

Undoing a LostFocus event

I have a requirement to detect an invalid value as a user tabs off a textbox, display a messagebox, reset the value in the textbox, and move focus back to the textbox.

The obvious solution is to handle the LostFocus event. My first attempt at this is shown below

Private Sub OverallDiscountPercentTextBox_LostFocus(sender As System.Object, e As System.Windows.RoutedEventArgs) Dim OverallDiscountAmount As Decimal = 0 Decimal.TryParse(OverallDiscountPercentTextBox.Text, OverallDiscountPercent) If OverallDiscountPercent > 100 Then MessageBox.Show("Discount cannot be more than 100%.", "Discount Error", MessageBoxButton.OK, MessageBoxImage.Error) OverallDiscountPercentTextBox.Text = 0.ToString("F2") OverallDiscountPercentTextBox.Focus Exit Sub End If ... End Sub
This does not move the focus back to the textbox. Clearly the framework has not finished moving the focus to the next control when the LostFocus event is raised. I suppose this doesn't happen until the next control's Focus event is raised.

The solution is fairly simple once you understand how to write lambda functions in Visual Basic. You defer the Focus method like this...

Dispatcher.BeginInvoke(Function() OverallDiscountPercentTextBox.Focus(), Windows.Threading.DispatcherPriority.Background)

Friday, July 4, 2014

Creating Styles and Binding Converters in code

For WPF 4.0

Recently a colleague of mine has been struggling to write generic code for assigning converters to control the background of a DataGridTextColumn. I pointed him to my April 24th 2014 blog entry entitled "NamedObject Error in Converters" which starts off by showing how to utilize a foreground and background converter in XAML for a DataGridTextColumn (and then goes on to break them).

He got that working but could not determine the code for implementing those same converters in code. While I don't normally feel the need to do something in code that can be done in XAML, I recognize that there are times when you don't know everything at design time and things have to be delayed to run time.

It really isn't too difficult to do this. You start by assigning an ElementStyle to the DataGridColumn, then creating a binding for the background and a multibinding for the foreground and binding them to the ElementStyle. The correlation between the code and the XAML it replaces is easy to see.

Here's the XAML with the style, bindings, and converters removed...


<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication6"
        Title="MainWindow" Height="350" Width="525" 
        
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <local:ForegroundConverter x:Key="ForegroundConverter"></local:ForegroundConverter>
        <local:BackgroundConverter x:Key="BackgroundConverter"></local:BackgroundConverter>
    </Window.Resources>
    <Grid>
        <DataGrid Name="DG" ItemsSource="{Binding Path=DT.DefaultView}" AutoGenerateColumns="False" CanUserAddRows="False">
            <DataGrid.Columns>
                <DataGridTextColumn x:Name="Description" Header="Description" Binding="{Binding Path=Description}" IsReadOnly="true"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>


The converters stay the same as you move from XAML to code...


Public Class ForegroundConverter
    Implements IMultiValueConverter

    Public Function Convert(values() As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IMultiValueConverter.Convert

        Dim IsError As Boolean = values(0)
        Dim IsWarning As Boolean = values(1)

        If IsError Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Red)
        End If

        If IsWarning Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Blue)
        End If

        Return New SolidColorBrush(System.Windows.Media.Colors.Black)

    End Function

    Public Function ConvertBack(value As Object, targetTypes() As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class

Public Class BackgroundConverter
    Implements IValueConverter

    Public Function Convert(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert

        If CBool(value) Then
            Return New SolidColorBrush(System.Windows.Media.Colors.Yellow)
        Else
            Return New SolidColorBrush(System.Windows.Media.Colors.White)
        End If
    End Function

    Public Function ConvertBack(value As Object, targetType As System.Type, parameter As Object, culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack
        Throw New NotImplementedException()
    End Function
End Class


And lastly here is the code that replaces the XAML. Note that about eight lines of simple XAML has been replaced by 13 lines of difficult to read code. And for anyone who might point out that code is reusable, I would point out that styles make XAML just as reusable.

Notice I do not attempt to set the Source property of the bindings. The binding will use the ItemsSource property of the column's DataGrid which itself makes use of the DataContext of the window. If we were trying to control properties of UIElements such as a Label we would need to set the binding's Source property ie Binding.Source = DT.Rows(0).

Imports System.Data

Class MainWindow

    Public Property DT As DataTable

    Public Sub New()

        ' Create and populate the DataContext
        DT = New DataTable

        DT.Columns.Add(New DataColumn("Description", Type.GetType("System.String")))
        DT.Columns.Add(New DataColumn("IsError", Type.GetType("System.Boolean")))
        DT.Columns.Add(New DataColumn("IsWarning", Type.GetType("System.Boolean")))
        DT.Columns.Add(New DataColumn("IsImportant", Type.GetType("System.Boolean")))

        DT.Rows.Add("Error", True, False, True)
        DT.Rows.Add("Warning", False, True, False)
        DT.Rows.Add("Info", False, False, True)

        InitializeComponent()

        ' Create ElementStyle
        Dim s As New Style(GetType(TextBlock))
        Description.ElementStyle = s

        ' Create background setter
        Dim bBinding As New Binding()
        Dim BackgroundConverter As Object = FindResource("BackgroundConverter")
        bBinding.Path = New System.Windows.PropertyPath("IsImportant")
        bBinding.Converter = BackgroundConverter
        s.Setters.Add(New Setter(TextBlock.BackgroundProperty, bBinding))

        ' Create Foreground setter
        Dim fBinding As New MultiBinding()
        Dim ForegroundConverter As Object = FindResource("ForegroundConverter")
        fBinding.Bindings.Add(New Binding("IsError"))
        fBinding.Bindings.Add(New Binding("IsWarning"))
        fBinding.Converter = ForegroundConverter
        s.Setters.Add(New Setter(TextBlock.ForegroundProperty, fBinding))

    End Sub
End Class


Monday, June 30, 2014

On the importance of empirical performance testing

Twice recently I thought I had an opportunity to improve performance in my WPF application. Twice I have been wrong and only realized this because I actually tested performance before and after the change.

Example 1:
I have a WPF page with eight combo boxes. I populate the combo boxes from lookup tables in the database. When I initialize the page I populate the combo boxes one at a time. This is fairly common practice.

It occurred to me that as the combo boxes are not dependent on each other I could populate them asynchronously and wait until they had all loaded before continuing. Theoretically this would mean that this section of code would only take as long as the loading of the slowest combo box.

I performed before and after performance tests using a Stopwatch object and found that the single-threaded synchronous code executed in 1.485 seconds on average and the asynchronous code executed in 1.820 seconds on average. In addition, the asynchronous code is less robust because I have to keep track of incomplete asynchronous requests. It became obvious that the original synchronous code was better so I kept it.

Example 2:
I have a requirement to display a datagrid of documents and to display the document number in black if the document has no errors or warnings, blue if it only has warnings, and red if it has any errors. The documents are in a table called GLDocument and the errors are in a linked table called GLDocumentErrors. There is a bit field in GLDocumentError called IsError which is true if the GLDocumentError is an error and false if it is a warning.

My original SQL had three separate subqueries, one to detect each condition.

SELECT D.ID,
  CASE (SELECT COUNT(*) FROM GLDocumentError E WHERE E.GLDocumentID=D.ID) WHEN 0 THEN 0 ELSE 1 END AS HasAnyErrors,
  CASE (SELECT COUNT(*) FROM GLDocumentError E WHERE E.GLDocumentID=D.ID AND IsError=1) WHEN 0 THEN 0 ELSE 1 END AS HasErrors,
  CASE (SELECT COUNT(*) FROM GLDocumentError E WHERE E.GLDocumentID=D.ID AND IsError=0) WHEN 0 THEN 0 ELSE 1 END AS HasWarnings
  FROM GLDocument AS D

It occurred to me that I can get the results I needed with a single subquery which should be a lot faster. By dividing the GLDocumentError row count into the sum of IsError I will get DBNull if there are no GLDocumentError records, 0 if there are only warnings, and > 0 if there are any errors. I just have to cast the IsError from a bit to an float so that I can SUM it and the division result will be a float too. The simplified SQL looks like this...

SELECT D.ID,
  (SELECT SUM(CAST(IsError AS float))/COUNT(*) FROM GLDocumentError E WHERE E.GLDocumentID=D.ID) AS ErrorProportion
  FROM GLDocument AS D

Because this only has one instead of three subqueries it should be a lot faster right? So I SET STATISTICS TIME ON and let rip. Here are the surprising results...

-- For the three subquery SQL
(139160 row(s) affected)
SQL Server Execution Times:
CPU time = 782 ms, elapsed time = 519 ms.

-- For the one subquery SQL
(139160 row(s) affected)
SQL Server Execution Times:
CPU time = 2033 ms, elapsed time = 2755 ms.


 
It appears all that CASTing and float division is more expensive than three almost identical subqueries. If you notice, the CPU time of the first query is more than the elapsed time. This tells me the query is being spread across multiple CPUs which accounts for the speed. The second query is probably executing on only one CPU.

So the moral of the tale is that you should always look before you leap!