A while ago, I put an example application on my blog on how to build an outlook style application.
The last couple of weeks, I’ve been working on a new version of this app. I’ve done some bugfixes, but also included support for opening use cases in a popup window. It’s turning out to be quite an advanced demo of what’s possible with Prism. But while I’m playing with it, I can’t help but be amazed with the things I can push Prism into doing :)
You will need the following references to make the solution work:
So there are a couple of things I didn’t explain in my previous blog:
- How to do ViewModel first development?
- How to open a Use Case in a popup?
- Why I’m using ObservableObject in my ViewModels?
So here goes:
ViewModel first development
I’m a big van of ViewModel first development. So what does that mean?
- I create my ViewModels before i create my Views. This allows me to create testable viewmodels that absolutely don’t rely on any visual aspects.
- My application code programs against ViewModels, not Views. Most of my code doesn’t need to know about the views. Only when a ViewModel needs to be displayed should you load up the view to display it.
There are a couple of big advantages to this approach:
- You can easily unit test your UI logic, without relying on visual elements.
- I can easily reskin my app.
- You don’t have to forward data from your views to your view models. This is something I have always found annoying with a View First approach. For example, if you have a view that displays a person and needs a PersonID to do it. With view first, your code has to talk to the view. So the view needs a PersonID property. But actually, the logic of your view needs that PersonID, so it needs to be forwarded to your ViewModel. Annoying, tedious, error prone!!!
I wanted to be able to put ViewModels into my regions and to have some code that would provide the visualization onto it. So the code I want to be able to write is:
// 1: Setup visualizations (typically in Module.Initialize)
// Whenever an EmailMainViewModel is displayed, visualize it with an EmailMainView
modelVisualizationRegistry.Register<EmailMainViewModel, EmailMainView>();
// 2: Add ViewModels to region (view discovery, view injection or any other method)
region.Add(new EmailMainViewModel());
ViewModel First internals
If you’re interested in how I built the ViewModel First code? here it is:
ModelVisualizer
The first step into creating the view model first appraoch was to create a ModelVisualizer class. The reason for this was: 'The prism region adapters will put the content of the regions directly into the control that hosts the region (for example, a contentcontrol). There is no extensionpoint to sit in between that.
So I created something that I could put in a region, that would hold BOTH the View and the ViewModel. It would be the glue between the view and the viewmodel and:
- Set the View as the content in the Visual Tree.
- Set the ViewModel as the datacontext for the View.
- Forward common information between the ViewModel and the View (such as the RegionContext or the IsActive values)
The following diagram explains my ModelVisualizer.
The ModelVisualizer IS a ContentControl. So it can be placed inside the Visual Tree. It will set the View as the content. It will also set the ViewModel as the datacontext of the view (so the view can bind to all the information in the ViewModel). Lastly, the IModelVisualizer implements the IRegionContextAware and IActiveAware interfaces, and will synchronize these interfaces with the View and ViewModel if they implement the interfaces.
Visualizing Region
The ModelVisualizer worked great. However, it demanded that all my code knew about the ModelVisualizer. I didn’t want to do that, because I wanted to make it implicit. There were some issues with that though, because due to the current implementation the region adapters, I didn’t have an extension point to create my visualizations implicitly. I couldn’t change the type of region that was created by adapters and I couldn’t change how the adapters set the content of the region to the hostcontrol. But I solved that with a little trick, that I call my VisualizingRegion.
The trick was: while I couldn’t change what type of region was being created, I could control what type of region was registered to the RegionManager (because there is a regionbehavior that does the registration. So I just wrapped the Region that was created by the regionadapters in my own region (the visualizing region) and registered my own visualizing region to the regionmanager.
The only way a consumer can get access to the Region is through the RegionManager, so that solved the problem :)
Easier Solution to region context
In Prism V2, we introduced the concept of RegionContext. The RegionContext is a way that a view that hosts a region can share some of it’s information with any childviews that are loaded into it’s region. While I really liked the concept of RegionContext, I didn’t really like the implementation of it.
So I created the IRegionContextAware interface. It has one property (the regioncontext as an ObservableObject) and the RegionContextAwareRegionBehavior that would sync the context of the region with the RegionContextAware properties.
Opening a Use Case (or ViewModel) in a popup
One thing I thought was an interesting challenge was opening a viewmodel in a (non modal) popup. So why would this be challenging:
- You can have many instances of the same popup open at the same time.
- Viewmodels shouldn’t know if they are opened in the main window (perhaps in several tabs) or in a popup. This should be decided by the designer, preferably in XAML.
The code I wanted to be able to write is:
1: // Create an UseCase to write new email messages
2: NewEmailUseCase newEmailUseCase = ApplicationModel.CreateObjectInScopedRegionManager<NewEmailUseCase>();
3:
4: // Add some data to the Email message use case (in this case, a blank new email)
5: newEmailUseCase.Message = new EmailMessage();
6:
7: // Show the email (in a popup, but the consumer doesn't know that)
8: ApplicationModel.ShowUseCase(newEmailUseCase);
The problem with opening several instances of the same popup is: Regions are identified by name in the regionmanager. If you have several popups of the same type open, each popup needs it’s own RegionManager, or else the region names will collide. In my scenario, I decided that the Popup has most of the same region names as the main shell. That way, any ViewModel or use case could be loaded in either the popup or the main window.
Creating use cases in a scoped regionmanager
Ok, so i need to create a scoped regionmanager. That’s not to hard, just do: RegionManager.CreateRegionManager() and you have one. However, I wanted the consuming code NOT to have to think about this. It should be blissfully ignorant about the fact that it’s in a scoped regionmanager or not.
Since all most of my objects are created by a DI Container, and it injects all of the dependencies to my object, I decided to use that to solve this problem. So I created a scoped Container and registered the scoped regionmanager with that. Then I created my use case from the newly created scoped container, and if it needs a regionmanager, it would automatically get the scoped regionmanager. Pretty neat huh.
So schematic, that looks like this:
So when the use case (in green) get’s created, it and it’s dependencies would get the scoped regionmanager injected. Without knowing about the scoped containers or scoped regionmanager.
Adding use cases to popups
Adding the use case to a popup (in the application model) works like this:
- Create the use case.
Your code creates a new use case instance, for example new NewEmailUseCase(); The application model has a method to create a use case within a new RegionManager scope (as described before). - Show use case.
Your code calls the Application model to show the use case. - Add to region.
The application model (which is logic) doesn’t even know if the use case is displayed in a popup or not. It just adds the use case to a region. The region itself is defined in the Shell.XAML. - Visualize the Use case
Just as a visualization (view) can be registered for ViewModels, i’ve also registered a visualization for Use Cases. In this case, I’ve registered a window (called Popup.XAML) as visualization. - Show /Close the popup on activate.
Now that the visualization is there, I had to create some code to show and close the popup. I decided that, if i Activate() the use case, the popup should be displayed. If I deactivate Use case, the popup should be closed and the use case should be removed from the region. This functionality I put in a RegionBehavior, because it can easily monitor if something is added or removed from a region. - Assign scoped region manager to the popup
Lastly, the popup window needs to know the RegionManager that the Use Case wants to use. So I’ve created the IRegionManagerAware interface. If a usecase implements that interface, anything that’s placed inside the region will get this region manager assigned.
As you can see, there are quite a lot of moving bits for showing a use case into a popup. Of course, it would have been A LOT easier to give ‘your code’ (an other use case or something) the knowledge that a view can be opened in popup.
ObservableObject for easier binding
If you look at my viewmodels, you’ll see that I make use of the ObservableObject a LOT. It’s a very simple object, one that just takes a type and wraps it in a INotifyPropertyChanged. The nice thing about this is, that I don’t have to add any INotifyPropertyChanged code in my ViewModels anymore. That makes it soooo much easier to do 2 way databinding in WPF.
The only problem is, that to get to the value, you often have to do: viewModel.MyProperty.Value to get to the actual value of MyProperty. But I thought that’s worth it since now I don’t have to write INotifyPropertyChanged code for my properties anymore.
Conclusion
Like I mentioned, before, this outlook style app has become quite an advanced demo of what’s possible with Prism. But I hope it gives you some idea’s on what’s possible with it. I’m also in the process of creating a video walkthrough of this application. But I’ll let you know when that’s done!
Keep practicing!
_Erwin
[UPDATE]
If you get an System.Threading.SynchronizationLockException from Unity: Don’t worry! I had turned on ‘break on all exceptions’ so the debugger is also breaking on these handled exceptions. Just continue and the app will run fine, or change that debug setting.
"
No comments:
Post a Comment