Writing a generic custom Prism RegionAdapter for a complex control is sometimes difficult, because custom RegionAdapters are custom. They normally have some specific logic built into them that make them fit into a particular application just right. You might need a tweak here or a tweak there, need a feature here or a feature there, all depending on the requirements of the application itself.
I have been asked numerous times about creating a RegionAdapter for the XamDockManager control that can be found in the Infragistics NetAdvantage for WPF product. I am always hesitant to write a RegionAdapter for a complex control because I never know how the control will be used and my implementation might not fit their application needs. Then they will think that Prism doesn’t work or the control is a piece of crap, and that I don’t know what the hell I’m doing. I always assume they will just modify the code to make it work, but I realize that sometimes they might not understand the technology well enough to make changes. So this blog will be my attempt to write a “generic” custom RegionAdapter for the XamDockManager control. Because this is a “generic” region adapter, I will outline the intended functionality of this RegionAdapter. I will explain exactly what is happening within each class to make it easier to make modifications if necessary. If you don’t care about the WHY, then just download the source and start using it.
- Only intended to support a TabGroupPane as a region. I didn’t want to make the XamDockManager itself a region because that would be too limiting. By using the TabGroupPane, I can define multiple regions within a complex xamDockManager layout.
- Will not support data binding to the TabGroupPane.ItemsSource. As with the default ItemsSourceRegionAdapter, this just causes major issues.
- All views injected must be of type UserControl. I simply want to create a UserControl that has my view elements and then inject just the UserControl. I don’t want to have to create controls that derive from ContentPane or create ContentPanes in code.
- Support declaring ContentPanes in XAML and injecting views into ContentPanes. This is helpful for when you have default views or hard coded views that are not dynamic in nature, but still need the ability to inject other views into the region.
- Views must be removed when closed. The XamDockManager gives you the ability to control whether ContentPanes are completely removed from the control or just hidden from view when closed (clicking the close button). My requirement is that when a view is closed, it will be completely removed from the region and the XamDockManager control. So I will not honor the IRegionMemberLifetime interface which allows you to keep a view alive.
- Support IActiveAware. If my View or ViewModel implements the IActiveAware interface, I want my RegionAdapter to also support this behavior. This lets me know which View/ViewModel is the active item in my region. This needs to be the case even if the ContentPanes are docked or floating.
- Have the ability to control the ContentPane.Header property. I want my View or ViewModel to have control of what the tab header displays.
The RegionAdapter is made up of three parts:
The TabGroupPaneRegionAdapter is the actual RegionAdapter that gets registered in the Bootstrapper of your prism application. It’s implementation is very simple and is as follows:
As you can see, we are going to rely on our TabGroupRegionBehavior to manage our Region views.
The TabGroupRegionBehavior class is doing all the heavy lifting. Let’s look at the code, and then talk abut some of the really important parts.
When this behavior is invoked on the Region, the first thing that happens is that we synchronize any views that may have been declared in XAML with any views that we are injecting. This makes sure that our Region will know about every view that has been placed inside of it. Even those not injected.
During this process, we take the views being injected and create ContentPanes for them. Why do we do this? The TabGroupPane can only contain ContentPanes as children. So we must create a new ContentPane for every view being injected and use that view as the Content for the ContentPane. This is what happens in the PrepareContainerForItem method. Notice that not only are we creating a new ContentPane and settings it’s Content property to the injected view instance, but we are also getting the DataContext of the view, which is most likely a ViewModel, and setting the DataContext of the ContentPane to be the same. This will ensure that all our data bindings will work as expected. We are also keeping track of if we generated the ContentPane. Remember how we support both ContentPanes declared in XAML and views that are dynamically injected? Well, we need to know which ones we created ContentPanes for. The IsGeneratedPropert allows us to do this. Now we know who created what. We also create some data bindings which we will get to later. The next important thing to make note of in this method is that we are adding an event handler for the Closed event of the ContentPane. This will let us know when we need to remove the view from the region. If you notice we set the ContentPane.CloseAction = PaneCloseAction.RemovePane. This will basically remove the view from the XamDockManager for us so we don’t have to worry about it.
The next thing that happens is we add an event handler for the XamDockManager.ActivePaneChanged event. This lets us know which ContentPane had been activated no matter where is has been dragged to. In this handler we first check if the ContentPane is being deactivated (e.OldValue) or activated (e.NewValue). The logic is very similar for both operations. First we check to see if the Region contains the view as is. Meaning that we are dealing with a pre-defined ContentPane most likely declared in XAML. If it is found, then we simply deactivate/activate it. If the Region doesn’t contain the view, then that means we are dealing with an injected view that has been wrapped in a generated ContentPane. So we simply grab the Content of the ContentPanel, because that will be the actual injected view, and then check to see if the region contains the view. If we find it, then we deactivate/activate it. Pretty simple stuff. I do want to note, that this is where the support for IActiveAware comes in. When we activate and deactivate the view, we are invoking the IActiveAware members through the base Prism behavior.
We also add an event handler for the Regions.Views.CollectionChanged event. This event executes when new views are added or removed from a region. So this is where we need to add and remove views to the TabGroupPane.Items collection. In this case we are only concerned with when new views are added. Not removed. Why do you ask? Remember when we set the ContentPane.CloseAction property to PaneCloseAction.RemovePane? Well, this is what actually handles removing the view from the TabGroupPane.Items collection. So we don’t have to mess with it. We only care about adding views. So when we get a new view to add, we simply call our PrepareContainerForItem method, and add the resulting ContentPane to the TabGroup.Items collection. Pretty simple.
The last thing to note is when we close a ContentPane. Remember we added the event handler for the ContentPane.Closed event? Well this is where we simply remove the view from the Region. First we make sure to unsubscribe to the ContentPane.Closed event (no memory leaks), and then we check to see if it was a predefined ContentPane declared in XAML or a view that was injected and wrapped in a generated ContentPane. We remove the view from the region and then clear out the Content property from the ContentPane to help eliminate any memory leaks.
In order to have the ability to set the Header of the tab, we need to create a data binding between a property on the View or ViewModel and the Header property of the ContentPane. To accomplish this I have introduced an interface called IDockAware.
It has a single property appropriately called Header. If your View or ViewModel implement this interface, the value you have set for the property will be used as the Header of the ContentPanel. How does this work? In the TabGroupPaneRegionBehavior.PrepareContainerForItem method, I am calling another method called CreateDockAwareBindings. This method checks to see if the View or ViewModel implements the IDockAware interface and if it does sets a data binding between the ContentPane.Header property and the IDockAware.Header property. This way you have complete control of the tab header and can even update it if necessary.
RegionAdapter in Action
Let’s see this baby in action. I already have a Prism application with a single module and a single view ready for testing. The first thing we need to do is register our RegionAdapter in the Bootstrapper.
Next, I have a Shell with some regions defined on a couple of TabGroupPanes.
As you can see I have even added a couple of “default” ContentPanes declaratively in XAML. In my Module I am simply injecting a few instances of my view into the two regions. Nothing special here so I won’t take up space with a code snippet. What I do want to share with you is the ViewModel that the view is using as it’s DataContext.
As you can see, this ViewModel implements the standard INotifyPropertyChanged, but it also implements two more interfaces. It implements Prism’s IActiveAware interface so I will always know which View/ViewModel is the active View/ViewModel. It also implements our newly created IDockAware interface, so I can set the Header of the View’s ContentPane. This is the result of the running application.
As you can see, I have multiple instances of my View injected into the various Regions. The ViewModel is properly setting the header property of the tabs, and I know which View is the active view as I change tabs or start docking and undocking tabs.
That just about wraps it up. Go ahead and download the source and start having some Prism fun. If something doesn’t fit your needs, feel free to change it so that it does. If you have any questions feel free to contact me through my blog, on twitter (@BrianLagunas), or leave a comment below.