Building HelloMEF – Part V – Refactoring to ViewModel
In the last post we migrated over to the new DeploymentCatalog. In this post we’ll look at refactoring the code to incorporate the MVVM pattern. Code from the last post is available here
Why ViewModel?
There’s a ton of content out there in the blogosphere that discuss the virtues of separated presentation patterns including MVVM. I am not going to reiterate those here, but I will point you at Josh Smith’s excellent MVVM article here. I’ll also shamelessly plug my own post where I shared my own view on the essence of MVVM (which is based on views of others i respect). In short it’s about decoupling UI logic for better maintainability which includes making it easier for a graphic designer to style the UI.
So where are the maintainability issues in the current “app”? Currently the main place is within MainPage.xaml.cs where you see the following code:
public void OnImportsSatisfied()
{
TopWidgets.Items.Clear();
BottomWidgets.Items.Clear();
foreach (var widget in Widgets)
{
if (widget.Metadata.Location == WidgetLocation.Top)
TopWidgets.Items.Add(widget.Value);
else if (widget.Metadata.Location == WidgetLocation.Bottom)
BottomWidgets.Items.Add(widget.Value);
}
}
The logic is simple yes, but it still has issues.
- The main issue is that it is mixing UI business logic with the rendering. The logic which determines which item goes to Top to Bottom is a completely separate concern from how those items actually get displayed. Having the logic mixed in means the code is very brittle where any slight change to the way the UI is rendered can break it. It also makes it difficult for a graphic designer from being able to change the look and feel of how the widgets are rendered.
- The second issue is that I am imperatively adding elements to the UI rather than letting the rich DataBinding engine do the work it was designed for. This makes it difficult to re-skin the UI.
- Third, in it’s current form, the logic is difficult to test, meaning if the logic breaks I am relying on QA / manual testing to pick it up.
So ViewModel will fix all of this?
I say with confidence Yes! Using ViewModel will allow us to refactor the logic into a nice reusable and testable place. It will also gives us the decoupling we need to leverage the Silverlight rendering engine to it’s fullest. Along the way you will also see how MEF can help us with regards to implementing MVVM within our applications. We’ll introduce 2 view models to help us get there. When we’re done, our application will look like this.
Pre-requisites.
As with the last post, you’ll need our MEF codeplex bits for following along. If you start where we left off in the last post you’ll be fine. You’ll also need a copy of Laurent Bugnion’s awesome “Glenn Block approved” :-) MVVM Light, which you can get here and which we’ll be using for it’s ICommand default implementation.
Let the refactoring begin!
First we’ll go create our ViewModel. We’ll start off with an empty class. Go add a new MainPageViewModel.cs class to the HelloMEF project. Leave it empty for now. Now let’s refactor MainPage to have MEF deliver it’s ViewModel. There’s a lot of religous debate in the community around View First vs ViewModel First construction of the UI. I am going to use View first, period, and no that’s not open for debate :-)
Refactoring MainPageView
What we’re going to do is rip out the logic in MainPageView, and replace it with importing it’s ViewModel and setting it to the DataContext. Replace MainPageView.xaml.cs the following:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
CompositionInitializer.SatisfyImports(this);
this.DataContext = ViewModel;
}
[Import]
public MainPageViewModel ViewModel { get; set; }
}
I think you’ll agree this code is a lot cleaner. My UI is now completely decoupled from any UI “logic”.
But WAIT, there IS code in the code behind, and the ViewModel police are probably on their way to my house. My answer when they show up is “it’s simple wiring logic, so WHAT!” That being said you can certainly look at ways to remove to right that small amount of code, but I am not losing any sleep over it. :-)
Notice I am not using constructor injection rather I am importing my ViewModel through a property. The reason is because I am allowing XAML to create my view rather than having MEF create it for me. SatisfyImports is then called to tell MEF to inject the view. It removes one level of mental gymnastics which in particular manifests itself when you start dealing with nested Views and ViewModels. If you’ve dealt with manually assembling Composite UIs / using regions, you know what I mean. It also allows this view to be more designer friendly as I can configure a design time VM to show up. That is for another post.
Fleshing out MainPage’s ViewModel
Now we’re go and finish the empty MainPageViewModel we created earlier. We’re not just going to move the code from the view into the model as we’d just be moving the problem. Instead we’re going to get rid of the imperative code and allow the view to render itself on the model through binding. We’ll still need to leverage MEF’s metadata and ensure that the widgets go in the right “place” wherever that happens to be. This raises a question as to how as previously there was a single collection of widgets that we imported into MainPage before and which we manually walked. The solution I chose was to create collection properties for each set of widgets. That allows the view to render it any way it wishes and decouples from the ItemsControls that were used previously.
Below is the code:
[Export]
public class MainPageViewModel : IPartImportsSatisfiedNotification,
INotifyPropertyChanged
{
[ImportMany(AllowRecomposition = true)]
public Lazy<UserControl, IWidgetMetadata>[] Widgets { get; set; }
public IEnumerable<UserControl> TopWidgets { get; private set; }
public IEnumerable<UserControl> BottomWidgets { get; private set; }
public void OnImportsSatisfied()
{
TopWidgets = Widgets.Where(w => w.Metadata.Location ==
WidgetLocation.Top).Select(i => i.Value);
BottomWidgets = Widgets.Where(w => w.Metadata.Location ==
WidgetLocation.Bottom).Select(i => i.Value);
OnPropertyChanged("TopWidgets");
OnPropertyChanged("BottomWidgets");
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string property)
{
var handler = PropertyChanged;
if (handler != null)
PropertyChanged(this, new PropertyChangedEventArgs(
property));
}
}
MainPageViewModel implements IPartImportSatisfiedNotification as the view did previously. It also implements INotifyPropertyChanged in order to tell the view whenever it is recomposed. It exports itself in order to make itself available to be imported by the view. MainPageViewModel imports all the Widgets as did the view, but it also exposes two different properties TopWidgets and BottomWidgets. Whenever recompostion occurs, OnImportsSatisfied is called which updates these properties with their own filtered collections by using a LINQ query with the appropriate metadata filter. It then raises PropertyChanged in order to update the UI. This really shines through the power of metadata and metadata views in MEF!
Updating the bindings
The last thing we need to do to get the ViewModel wired is to update the view to bind the widget ItemControls to the appropriate properties. Replace the StackPanel in MainPage.xaml.
<StackPanel x:Name="LayoutRoot" Background="Black">
<Border Background="Cyan">
<ItemsControl x:Name="TopWidgets"
ItemsSource="{Binding TopWidgets}" Height="Auto"
FontSize="30">
</ItemsControl>
</Border>
<Border Background="Yellow">
<ItemsControl x:Name="BottomWidgets"
ItemsSource="{Binding BottomWidgets}" Height="Auto"
FontSize="30">
</ItemsControl>
</Border>
</StackPanel>
We’re almost there
Build and execute the app. You should see that everything works as before only now our new ViewModel is in place. You can now easily go and and reformat the UI without impacting the underlying ViewModel at all. We could stop here as we’ve already made a good leap in improving the maintenance of our app. We won’t though, there’s one more culprit we can refactor. Widget1.
What’s wrong with Widget1?
[ExportWidget(Location = WidgetLocation.Top)]
public partial class Widget1 : UserControl
{
[Import]
public IDeploymentCatalogService CatalogService { get; set; }
public Widget1()
{
InitializeComponent();
Button.Click += new RoutedEventHandler(Button_Click);
}
void Button_Click(object sender, RoutedEventArgs e)
{
CatalogService.AddXap("HelloMEF.Extensions.xap");
}
}
Widget1 contains logic which calls the DeploymentCatalogService to download a XAP, it’s not horrible but it has some of the same problems as MainPage did.
It tightly couples the download logic to the UI. This means if you refactor the UI there is a decent chance it will break.
- Downloading widgets is a completely non-UI concern which does not belong in the view.
- It means a designer can’t change from using a button without modifying the code.
The logic is not reusable, you can’t reuse it across multiple views. This may or may not matter depending on the specific case.
As small as it is, it is hard to test. That means if someone breaks the code and it no longer works, the only way to pick it up is through QA/automated UI testing. We want to pick up as many bugs as we can up stream rather than waiting for them to be caught.
Refactoring Widget1 to use a ViewModel.
Widget1 can be refactored to use it’s own ViewModel. We can then refactor the logic out of the code behind and move it into the model leveraging Silverlight 4’s new commanding support, Laurent’s MVVM light library, and a little MEF to make it more maintainable.
We’ll do this again with the ViewFirst approach :-) This time we don’t need to use CompositionInitializer though as our ViewModel will get discovered by the catalog. Add a new Widget1ViewModel.cs file to the HelloMEF project. Second add a reference to GalaSoft.MVVM light (in your “ProgramFiles\Laurent Bugnion (Galasoft)\Mvvm Light Toolkit\Binaries\Silverlight” folder). Now paste in the following code to Widget1ViewModel.cs.
using System.ComponentModel.Composition;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;
namespace HelloMEF
{
[Export]
public class Widget1ViewModel
{
public Widget1ViewModel()
{
Download = new RelayCommand(() =>
CatalogService.AddXap("HelloMEF.Extensions.xap"));
}
[Import]
public IDeploymentCatalogService CatalogService { get; set; }
public ICommand Download { get; private set; }
}
}
Widget1ViewModel is an export similar to MainPageViewModel. It imports IDeploymentCatalogService in order to allow downloading XAPs. It then exposes a Download command which is implemented using MVVM Lights’ (contributed by Josh Smith) RelayCommand to download the extensions.
Updating Widget1View
Once we have that in place we can clean up the Widget1 view. Paste the following into Widget1.xaml.cs.
[ExportWidget(Location=WidgetLocation.Top)]
public partial class Widget1 : UserControl
{
public Widget1()
{
InitializeComponent();
}
[Import]
public Widget1ViewModel ViewModel
{
get{ return (Widget1ViewModel) this.DataContext;}
set { this.DataContext = value;}
}
}
First I removed the previous UI logic. Then I added an import of the MainPageViewModel and set it to the DataContext. Constructor injection was also an option through usage of our ImportingConstructor attribute. I chose not to for consistency and in order to not increase the concept count. Feel free to use either approach though.
Now that we’ve updated the code behind we can update the XAML to bind to the new DownloadCommand. Replace the Grid element in Widget1.xaml with the following.
<Grid x:Name="LayoutRoot" Height="Auto">
<Button x:Name="Button" Content="Hello MEF!"
Command="{Binding Download}" Width="300" Height="100">
</Button>
</Grid>
Run the app!
Build and run the application, press the top button and you should see a screen that is quite familiar by now :-)
We’re done.
What did we gain?
You might be sitting there thinking wow that was fun, but what did it buy me? Here’s a quite summary:'
- MainPage and Widget1 are completely decoupled from the UI logic.
- MainPageViewModel and Widget1ViewModel are easily testable.
Both of which translate into less pain for us as our app evolves.
How did MEF help?
MEF enabled us to put the decoupled pieces back together. To be fair, we could have used other mechanisms. However MEF offered a nice solution in the box, which was convenient as our app was already using it :-) MEF also provided us CompositionInitializer which made it very easy to compose our UI parts without having to jump through a lot of hoops.
What’s next?
In this post I mentioned testing several times though notice I did not show ANY tests. That was deliberate :-) The reason is because I wanted you to see the value of ViewModel beyond the simple testability aspects, I wanted you to focus on how it improved our code whether we were testing it our not. In the next post we’ll look at how we can test it.
As always, code is attached.
Comments
Anonymous
March 08, 2010
The comment has been removedAnonymous
June 28, 2010
Please do add a sample that has main application interacting with Dynamically loaded XAP, sending parameters to it etcAnonymous
July 27, 2010
Oh.. so close to being the perfect example, but shouldn't you expose pure data classes in your viewmodel and not usercontrols? And then use itemtemplate on your itemscontrols?Anonymous
July 28, 2010
Lars It depends. In this case the ViewModel knows nothing about the widgets other than the fact that it needs to bind to them. Having DataTemplates would force exporting the template and putting a bunch of infrastrcture in place. codebetter.com/.../hello-mef-in-silverlight-4-and-vb-with-an-mvvm-light-cameo.aspx