Porting a WPF App with the Model-View-ViewModel Design Pattern to Silverlight 4

23 Mar
2010

Tim Heuer wrote glowingly about Josh Smith’s new book ‘Advanced MVVM‘ on his blog recently (http://timheuer.com/blog/archive/2010/03/04/advanced-mvvm-book-for-silverlight-wpf.aspx), so I decided to check it out.

I was reading through the comments, and saw that Josh recommended checking out an article on MSDN he’d written in Feb 2009 entitled: "WPF Apps With The Model-View-ViewModel Design Pattern" and get a firm grounding in that before moving on to his new book. This seemed like a good idea, so I decided I’d try migrating the WPF example app in the article over to Silverlight 4 as a way of trying out some of the new features like support for Commands, IDataErrorInfo support, etc.

In line with the Pareto principle, most of the migration went very smoothly and swiftly and then there were a few snags which ended up taking the bulk of the time to resolve. I’ve outlined these issues and their resolutions below in the hope that it helps someone doing something similar in future. The full completed Silverlight 4 solution is also included at the bottom of the post.

 

Silverlight version vs WPF version
Silverlight version vs WPF version


Issue One: No CollectionViewSource.GetDefaultView in Silverlight

Josh uses the CollectionViewSource.GetDefaultView method in the SetActiveWorkspace method to retrieve the default view of the Workspaces ObservableCollection, and then move the current item in that view to the workspace which has been passed in, using MoveCurrentTo. This is used when one of the Hyperlinks in the left hand pane is clicked, which results in a new tab being added to the workspaces tab control. MoveCurrentTo is used to set this latest item as the CurrentItem, which results in that tab being selected in the tab control.

Unfortunately Silverlight 4 doesn’t implement the GetDefaultView method on CollectionViewSource, so we have to retrieve a view from CollectionViewSource in an alternative way. I did this by changing the SetActiveWorkspace method to take an additional parameter in the form of an instance of a CollectionViewSource. The view is then retrieved using the View property of this instance, and the rest of the code works as expected.

To allow for an instance of a CollectionViewSource to be passed in, we define a public property called WorkspacesViewSource, and then initialise it in the constructor of the ViewModel, and set the instance’s Source property to be the Workspaces ObservableCollection.

This necessitates further changes though, as we now need to bind our Workspaces tab control ItemsSource property directly to the View property of our public CollectionViewSource: WorkspacesViewSource. We can do this in the Xaml of the TabControl using the syntax: ItemsSource="{Binding WorkspacesViewSource.View}". We now also need to raise the property changed event for WorkspacesViewSource when a workspace is added or removed, so we call our base.OnPropertyChanged method in the OnWorkspacesChanged event.

While this worked, it was not enough to get the Workspace tabs displaying in the tab control, due to the next problem…

 

Issue Two: Unable to cast object of type ‘MVVMSilverlightDemoApp.ViewModel.AllCustomersViewModel’ to type ‘System.Windows.Controls.TabItem’.

When a new workspace is added to the collection and the ItemsSource for the Workspaces tab control is refreshed, an ArgumentException was being thrown: Unable to cast object of type ‘MVVMSilverlightDemoApp.ViewModel.AllCustomersViewModel’ to type ‘System.Windows.Controls.TabItem’.

This is being throw by the OnItemsChanged event in the TabControl class. Luckily, we can look at the source code of this class as it is in the Silverlight Toolkit. In the OnItemsChanged event, we see that the TabControl casts each item in its ItemSource as a TabItem, and then if it fails the cast, it throws the ArgumentException.

There were a couple of options to solving this. We could subclass the TabControl and override the OnItemsChanged event to deal with our workspace items better. I started doing this, but it started rapidly escalating as there were a few Private and Internal methods and variables that the event utilised, so it began to look like I’d need to duplicate and modify the entire TabControl class, along with its dependant classes. Entirely possible, given that the source is all open, but I thought this was starting to diverge from the point of the exercise.

A quick search for the issue came up with a post on the Silverlight forums where it was mentioned that "TabControl doesn’t override PrepareContainerForItemOverride, so it won’t automatically wrap your data source in TabItems". The post suggested that the solution was to use a value converter on the ItemSource to turn each item into a TabItem.

I thought I’d try overriding the PrepareContainerForItemOverride method in my subclassed TabControl first, however this didn’t change the ArgumentException in the OnItemsChanged event, so I added a converter: WorkspacesTabConvertor.

This worked, in that the TabControl now displayed a tab for each item, and it was rendered correctly. However, each time the ItemsSource was updated (by adding or removing a workspace) all the tabs were recreated, which meant any selections in the AllCustomersView or data entered in a CustomersView were lost.

I settled on a workaround where I created a custom collection of TabItems based on ObservableCollection called WorkspaceTabItemCollection, and gave it AddItem and RemoveItem methods which took a workspace and turned it into a TabItem and then added or removed it from the collection. Then this collection was bound to the workspaces tab control’s ItemsSource. As it was a collection of TabItems, it is displayed correctly by the TabControl.

As a quick and dirty way of getting WorkspaceTabItemCollection working, I was creating the TabItem contents by instantiating a new AllCustomersView or CustomerView as a UserControl, and passing in the workspace as the DataContext. However, this required the WorkspaceTabItemCollection to know about the various views, so I refactored it to get the appropriate data templates from the MainPageResources.xaml file and apply them instead. There’s a bit of a hack still in place to set the new TabItem as IsSelected=true, to make it become selected in the TabControl, as the Silverlight TabControl doesn’t support the IsSynchronizedWithCurrentItem property, rendering the first issue with SetActiveWorkspace mostly irrelevant.  Oh well.

Workspaces are added to or removed from the WorkspaceTabItemCollection collection by hooking into the OnWorkspacesChanged event.

This worked well and things were starting to take shape. The next step was to get the selections in the AllCustomersView DataGrid to be totalled and display in the ‘Total selected sales’ area.

Tabs working
Tabs working

 

Issue Three: Can’t bind to DataGridRow IsSelected using a Setter Property in Silverlight

In the WPF version, Josh uses a Setter on the IsSelected property in the DataGridRow style to TwoWay bind to the DataGridRow’s underlying item’s IsSelected property. Then in the AllCustomersViewModel, this is detected in the OnCustomerViewModelPropertyChanged event, and the TotalSelectedSales property is in turn updated. This returns the sum of all TotalSales of each customer who’s IsSelected flag is set as true, and is the property that the ‘Total selected sales‘ area is bound to in the view.

Unfortunately, we can’t bind to the IsSelected property of a DataGridRow in Silverlight as it’s not publicly accessible. So we need another way to figure out what gets selected in the DataGrid and when.

After a bit of digging I found a great post by Alexey Zakharov which suggests using the CommandBehaviourBase class from Prism, and then add an attached property for the SelectionChanged event on the DataGrid.

We can then bind to a SelectedItemsChangedCommand in our ViewModel, which calls a SetCustomersAsSelected method and passes it a reference to the DataGrid (via the CommandParameter). Once we have the reference to the DataGrid, we can access the SelectedItems property of the DataGrid, and update the IsSelected property on our customers which will update TotalSelectedSales as detailed above.

This worked nicely, although I’m not entirely happy about the ViewModel needing to about a type of UI control (DataGrid). At least it avoids being tied directly to the View though.

With the AllCustomersView working nicely, we move on to the CustomerView form.

Selection working
Selection working

 

Issue Four: Silverlight data validation needs to match the WPF sample

The CustomerViewModel translated across to Silverlight with only one slight issue: Josh uses CommandManager.InvalidateRequerySuggested to dirty the commands registered with the CommandManager when an error is raised, which is used in turn to enable/disable the Save button on the form (as it is bound to a Command, so is only enabled if the Command returns CanExecute as true)

CommandManager isn’t implemented in Silverlight 4 at present, so we use RaiseCanExecuteChanged directly on our SaveCommand instead. To enable this, I added two extra methods to Josh’s RelayCommand class: OnCanExecuteChanged and RaiseCanExecutedChanged, using code from an example at: http://www.silverlightshow.net/items/Silverlight-4-How-to-Command-Control.aspx

This enabled or disabled the Save button in the View as appropriate, however I wanted the validation error messages to match the WPF sample. To do this, I restyled the TextBox and ComboBox controls in the View to remove the validation tooltip, by clearing out the ValidationToolTipTemplate control template.

Then I got the validation messages displaying in TextBlocks under each control by changing the binding Josh was using from: {Binding Path=(Validation.Errors).CurrentItem} to: {Binding (Validation.Errors)[0].ErrorContent} instead.

The last part was to get the validation on the TextBoxes to fire per keypress, rather than just when they lose focus. In WPF, you use: UpdateSourceTrigger=PropertyChanged to achieve this, however that doesn’t exist in Silverlight 4. There are a few posts on different ways of how to simulate this in Silverlight, but I liked the approach of Thomas Claudius Huber: http://www.thomasclaudiushuber.com/blog/2009/07/17/here-it-is-the-updatesourcetrigger-for-propertychanged-in-silverlight/ best.

After attaching helpers:BindingUpdateHelper.UpdateSourceOnChange="True" to each TextBox, they validated on each keypress, and all was good.

Validation
Validation

 

Issue Five: Unit Tests!

Josh kindly included quite comprehensive unit tests in his sample project, so I wanted to get them working in the Silverlight version as well.

I started off with a standard C# Unit Test project, and copied the tests across and updated any namespaces. I also needed to tweak the tests which verify the CurrentItem property of the CollectionViewSource.

Attempting to run the tests resulted in a FileNotFoundException: "System.IO.FileNotFoundException: Could not load file or assembly ‘System.Xml.Linq, Version=2.0.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35′ or one of its dependencies. The system cannot find the file specified". At this point I was using Linq to XML (although refactored it out later on, so the project no longer needs it), and wanted to figure out what the problem was.

Turns out that the Unit Test project was using the version 4.0 System.Xml.Linq.dll, and so there was a mismatch between the versions. The quick solution was to hack the .csproj file (Right click on the project in Visual Studio -> Unload Project -> Right click again and choose Edit -> make your changes and save -> then Right click and choose Reload) to give a HintPath to the reference to point it to the Silverlight version like so:

<Reference Include="System.Xml.Linq">
  <HintPath>$(ProgramFiles)\Microsoft SDKs\Silverlight\v4.0\Libraries\Client\System.Xml.Linq.dll</HintPath>
</Reference>

After fixing that, most of the tests passed, apart from any that were using the CustomerRepository. This turned out to be an issue with the GetResourceStream method, which uses Application.GetResourceStream to load in the customers.xml data file. When running via the Unit Tests, this was throwing a DllNotFoundException: "Unable to load DLL ‘agcore’: The specified module could not be found", as the Silverlight process wasn’t running, so the Application class couldn’t be accessed.

The quick solution to get the tests passing was to catch the DllNotFoundException and load the resource file using a standard StreamReader instead. A better long term solution would be to use something like Dependency Injection or Mocks instead.

The tests all passed fine after this, so I created a Silverlight Unit Test project as well to test them in there. They passed fine in this as well. You can run the tests by right clicking the SilverlightUnitTestsTestPage.aspx page in the MVVMSilverlightDemoApp.Web project and choosing View In Browser. This will launch a browser window with a Silverlight test harness which will run all the unit tests and show the results.

Unit Tests
Unit Tests

So, that’s it. You can download the solution below and have a play around and compare it to Josh’s WPF original. Hope it helps.

kick it on DotNetKicks.com

5 Responses to Porting a WPF App with the Model-View-ViewModel Design Pattern to Silverlight 4

Avatar

Michael Washington

March 27th, 2010 at 5:18 pm

Thank You. This is a great contribution to the community.

Avatar

Phil

March 30th, 2010 at 3:17 pm

Hi Edward, great post – did you get past the agcore.dll issue?

We have the same problem on the build server but not locally.

Avatar

Ed

March 30th, 2010 at 3:39 pm

Hi Phil. I hacked around the agcore.dll exception by catching the ‘DllNotFoundException’ specifically, and then using non-Silverlight specific code in the catch block, but I’d imagine that this won’t always be practical. From what I could make out, it’s thrown whenever you try to access the Application class, or anything specific to Silverlight that is established when it’s running inside the browser. Odd that it’s happening for you just on the build server though, and not locally. What happens if you try running the Silverlight Unit Tests (i.e. in the browser) on the build server?

Avatar

Design Pattern MVVM avec une DataGrid

February 21st, 2011 at 6:46 pm

[...] aller plus loin: http://edventuro.us/2010/03/porting-a-wpf-app-with-the-model-view-viewmodel-design-pattern-to-silver… Link to this post! Cette entrée a été publiée dans Silverlight, avec comme mot(s)-clef(s) [...]

Avatar

Porting a WPF App with MVVM Design Pattern to Silverlight 4 using Caliburn, FluentValidation « Exploring the world …

August 16th, 2011 at 7:05 am

[...] There is another blog entry describing how to migrate it into Silverlight 4 here: http://edventuro.us/2010/03/porting-a-wpf-app-with-the-model-view-viewmodel-design-pattern-to-silver… [DemoApp2] [...]

Comment Form

top