Sunday, July 22, 2012

First Impressions: Working with Prism in WPF

Three weeks ago, I started working with Prism on a WPF application.  Here are a few of my first thoughts:

The Good
Prism is an extremely powerful framework.  It provides classes to help you manage all parts of your WPF application (or Silverlight or Windows Phone 7 -- I'm sure that Metro will be coming soon).  So far, I've worked with the following features:
  • Modules / Module Catalog
  • UI Composition  / Regions / Region Manager
  • Navigation
  • Dependency Injection
  • Event Aggregation
  • Other "Helper" Objects
The Bad
Prism is not for beginning programmers.  If you do not have a solid grasp on intermediate .NET topics, then much of the Prism framework is going to look like magic incantations.  This can only lead to misuse of the framework classes, and most likely this misuse will lead to not getting the benefits (such as good separation of concerns, re-use, and testability).

Before using Prism, you should have a good foundation with the following topics:
  • Dependency Injection / Inversion of Control
  • Interfaces
  • Delegates / Func<T> / Action<T>
  • Lambda Expressions
  • Events and Event Handlers
  • Model-View-ViewModel (MVVM)
  • Various other Design Patterns
As a side note, the documentation ("A Developer's Guide to Microsoft Prism 4" -- available as a PDF or as a tree-book) has an appendix dedicated to the primary design patterns used by Prism.  This includes Adapter, Application Controller Pattern, Command Pattern, Composite and Composite View, Dependency Injection Pattern, Event Aggregator Pattern, Facade Pattern, Inversion of Control Pattern, Observer Pattern, Presentation Model Pattern, Registry Pattern, Repository Pattern, Separated Interface and Plug-In, and Service Locator Pattern.

Good Collection of Features
Prism is an extremely powerful framework and has gone through several iterations and refinements.  The current version (4.1) even includes support for Silverlight 5.  The Patterns & Practices team has obviously put a lot of work into the framework, and my experience so far has been bug-free (from the framework perspective at least -- I'm working on making my own code bug-free).  Let's take a closer look at the features that I've experienced so far:

Modules / Module Catalog
Modules allow you to separate your application functionality into discrete units that can be "plugged in" to the application.  The modules can (should?) be designed to be application agnostic and self-contained.  This means that they can be re-used in other applications.  For example, if you have a need for Customer Maintenance, this could be its own module.  If you have proper separation of concerns, then this module could be used in multiple applications.

For the application I am working on, we have modularized most of the application "screens".  These are being tied together through a work flow.  As a (fictional) example, to take a customer order over the phone, you could start with the Customer module (either entering new customer data or selecting an existing customer), moving on to a Shopping Cart module (where products and quantities are selected), then to a Shipping module, then to a Payment module.  If these modules are designed to work in isolation, then they can be reused in the same application in different work flows.

The Module Catalog keeps track of all of the modules in the application.  This can be set explicitly by specifying types and assemblies, or it can be done much more dynamically.  In our project, the bootstrapper process (that loads the module catalog) searches all of the assemblies in a particular folder and catalogs / initializes any modules that it finds in there.

UI Composition / Regions / Region Manager
The Region Manager is another very powerful tool.  It lets you associate different Views with different Regions of the UI.  This allows you to bring several Views together into a single "screen" for the user.  Different Views can interact with each other (through the Event Aggregator) and still remain programmatically isolated.  The idea is very similar to how a Master Page in an ASP.NET application allows you to have multiple web forms shown in different areas of a single "page".

Navigation
The Navigation infrastructure adds quite a bit of functionality to moving between views.  For example, what do you do if you have unsaved data in a View and the user wants to navigate to another View?  WPF does not have anything built in for this scenario, but Prism does.

Prism offers the IConfirmNavigationRequest interface that can be added to your Views or ViewModels (as a side note, many of the Prism features can be added to either the View or the ViewModel and they behave the same -- this gives you great flexibility depending on whether you are coding View-first or ViewModel-first in your application).  This interface has a ConfirmNavigationRequest method.

When using the Navigation infrastructure, you call "RequestNavigate" (notice the word "request").  The method parameters include a callback that runs when the navigation completes successfully.  But, when RequestNavigate is called, ConfirmNavigationRequest is fired on the View/ViewModel that you are navigating from.  This gives that View/ViewModel the chance to confirm or cancel the navigation.  For example, if the current View/ViewModel has unsaved data, it can prompt the user to Save, Discard Changes, or Cancel.  Cancel would then cancel the navigation, and the current View would remain in place.  Otherwise, the "from" View/ViewModel can simply call the callback, and navigation completes normally.

There are also other methods in the interface (part of INavigationAware and IConfirmNavigationRequest) that allow you to run code "OnNavigatedFrom" (to clean up any resources in the current View/ViewModel) or "OnNavigatedTo" (to initialize the new View/ViewModel).

This functionality is very powerful, and if you did not use what comes with Prism, you would probably end up building a lot of this yourself.  I speak from experience: I implemented a much simpler ICloseQueryable interface on a simple form manager used in a non-Prism application (for more information, see XAML App Review - Part 2: Form Manager).

Dependency Injection
You won't be able to use Prism effectively without dependency injection.  In order to have good separation of concerns for the modularity, MVVM presentation pattern, and other cross-cutting concerns, Prism needs a dependency injection container.  Prism ships with implementations for using Unity (from Patterns & Practices) and MEF (built into .NET 4.0), but you can build your own implementations for using a different DI container (or just cruise the web; I'm sure other people have created implementations for the most popular DI containers).

I've been working with Unity on our project.  With Unity, you can use both Constructor Injection (where the dependencies are specified as parameters in the constructor) and Property Injection (where public properties are flagged as "Dependency" and automatically resolved).

Dependency Injection lets your Views/ViewModels get access to the cross-cutting classes in your application.  For example, you could have an Authorization or Logging service/class that gets injected into each View/ViewModel.  This way, if the Module is used in a different application, the ViewModels will automatically get the cross-cutting services that are registered for that application.

In a Prism application you will (hopefully) make heavy use of interfaces.  For example, the ViewModel could have a dependency on a Model interface, and this dependency could be injected with constructor injection.  This gives you the flexibility of swapping out the Model when you want to unit test the ViewModel.

In this scenario, the application would use the DI container to associate the IModel with the concrete Model (with "RegisterType" if you're using Unity).  If this registration is done at the Module level, then the dependency can be correctly injected everywhere it is used. In the unit tests, you could create a mock of IModel that is registered with the test container.  This makes it very easy to unit test your ViewModel without having to worry about a specific implementation of that Model (such as one that needs a network connection to a service).

I'll be talking more about Unit Testing with Prism and Unity in a future article.  That's an interesting topic in and of itself.

Event Aggregation
With all the loose-coupling that is going on in Prism, it could be very difficult to communicate between modules.  This is where the EventAggregator comes in.  The EventAggregator allows one module to Publish an event (with a particular payload), and any other module can Subscribe to that event.  The two modules do not need to know directly about one another.  One is simply publishing an event, and the other is simply listening for an event.  Neither cares about where the event is coming from nor where it's going.

The EventAggregator takes things another step beyond the normal eventing model in .NET.  The Subscribe method also gives you the opportunity put in a condition based on the payload.  For example, you could say that you want to Subscribe to a StockUpdateEvent, but only if the payload has a StockTickerID of "GM".  The event is published normally, but your event handler only fires if the payload has that particular value in it.

Other "Helper" Objects
Prism also provides helper objects that are designed to make life easier (often by reducing boiler-plate code).  Two of my favorites are NotificationObject and DelegateCommand.

The NotificationObject (Prism) is a base class that implements INotifyPropertyChanged (.NET).  Pretty much all of your ViewModels and/or Models will need to implement INotifyPropertyChanged in order for the data binding to behave as expected.  NotifcationObject is a concrete class that implements INotifyPropertyChanged, so you don't need to include the boiler-plate code for that interface in every single object.

DelegateCommand (Prism) simplifies commanding and the implementation of ICommand (.NET).  Commanding is a much larger topic, but the usual process is to create a class that implements the ICommand interface (this can be a class embedded in the class that uses it or a completely separate class).  ICommand has 2 primary methods: Execute (which is the command code) and CanExecute (which determines whether the command can be run -- this can enable/disable a button tied to the command based on a particular state).

DelegateCommand replaces this separate class with a constructor that takes 1 or 2 delegates as parameters. The first delegate is the code for the command to run ("Execute") and the second delegate is for the "CanExecute" (if you don't provide this, it will always be "true").  Again, this class helps to cut down the boiler-plate code of creating an ICommand object and eliminates the need for a separate type.  Since it is part of your class, the delegates also have access to the internal members/state of the containing class.

Putting It All Together
These features all come together with the goal of making the application easier to maintain, more testable, and reusable.

One main thing to note: you don't have to use all of these features.  If you decide that you do not want to use the Region Manager or Navigation, that's fine.  If you don't want to use the Event Aggregator, that's fine, too.  You can use just the features that you want.  Also Prism does not assume that you are using the MVVM pattern; you don't need to use MVVM, but Prism provides helpers to make the pattern easier to implement.  And even though the features are mostly optional, I think that you'll find yourself using most of these features for any non-trivial application.

I've read about Prism (and previously about the Composite Application Guidance for WPF) and have found it interesting.  At the time, I was concerned about a lot of the apparent magic that is going on (and I am still concerned about this).  It is imperative that you have a good handle on the foundational principles used in Prism before you get started.  If you have a good foundation, then you can build very solid (yet de-coupled, maintainable, testable, and modular) applications.

Three weeks is not that long to have worked with a framework such as Prism.  I'm amazed at how much more I know about Prism than I did prior to this project; much of this is due to a couple of folks on the team who have used Prism successfully in prior projects.  There's still some learning to do on the finer points, and we are still tweaking our implementations a bit, but the larger functionality has fallen into place.

[Update: Here are some thoughts after working with Prism for a while: Five Months In: Working with Prism in WPF]

Happy Coding!

No comments:

Post a Comment