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.