WPf and Prism Tab Region Adapter, Part 02.

Sunday, July 04, 2010 6:02 PM | Leave a reply »

As we saw in the previous post, it’s pretty complicate to create a custom design in WPF to override the default style of the TabControl, but it’s pretty simple to extend the behavior of it.

As a senior dev, I usually don’t like to: 1) touch what is already working, 2) reinvent the wheel just to write the same code twice. For that there is the refactoring process, at most!

So, let’s start by the requirement we had in the previous post, we need to emulate the VS IDE in our applications, that’s it! We also saw that Prism has the RegionAdapter, so now we just need a cool control. Well there is the Avalon Dock project that is open source, really well done, flexible and ready for WPF 4. So let’s use it for our purposes. The final result should be something like this:

image

Avalon dock is crazy powerful and allows you to build a complete system of docking and modal windows into your WPF application. But before doing that you need to write a custom region adapter for it!

So, this is the basic concept of a custom Region Adapter in Prism. You create you custom adapter class by inheriting from RegionAdapterBase<T> in the following way:

Region adapter for Avalon Dock
  1. public sealed class AvalonRegionAdapter : RegionAdapterBase<DocumentPane>
  2. {
  3.     public AvalonRegionAdapter(IRegionBehaviorFactory factory)
  4.         : base(factory)
  5.     {
  6.  
  7.     }

 

Avalon dock has a lot of future, and you should create a Region adapter able to attach any type of dock view. In my case I will use only the Tab region adapter that is called DocumentPane. Now the RegionAdapterBase requires that you implement three methods:

Create region
  1. protected override IRegion CreateRegion()
  2. {
  3.     return new AllActiveRegion();
  4. }

 

The create region which specify what base adapter you want to use. In this case we want to handle any type of view added to this adapter, like an ItemsContainer or a TabControl.

Adapt
  1. protected override void Adapt(IRegion region, DocumentPane regionTarget)
  2. {
  3.     region.Views.CollectionChanged += delegate(Object sender, NotifyCollectionChangedEventArgs e)
  4.     {
  5.         OnViewsCollectionChanged(sender, e, region, regionTarget);
  6.     };
  7. }

Then we override the adapt method. This method is called once so in my case, because I have a DocumentPane I will then listen for the event Views.CollectionChanged. In this way I know every time if a view has been added or removed from the region.

Code Snippet
  1. private void OnViewsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e, IRegion region, DocumentPane regionTarget)
  2. {
  3.     if (e.Action == NotifyCollectionChangedAction.Add)
  4.     {
  5.         //Add content panes for each associated view.
  6.         foreach (object item in e.NewItems)
  7.         {
  8.             UIElement view = item as UIElement;
  9.  
  10.             if (view != null)
  11.             {
  12.                 DockableContent newContentPane = new DockableContent();
  13.                 newContentPane.Content = item;
  14.                 //if associated view has metadata then apply it.
  15.                 newContentPane.Title = view.GetType().ToString();
  16.                 //When contentPane is closed remove the associated region
  17.                 newContentPane.Closed += (contentPaneSender, args) =>
  18.                 {
  19.  
  20.                 };
  21.                 regionTarget.Items.Add(newContentPane);
  22.                 newContentPane.Activate();
  23.             }
  24.         }
  25.     }
  26.     else
  27.     {
  28.         if (e.Action == NotifyCollectionChangedAction.Remove)
  29.         {
  30.  
  31.         }
  32.     }
  33. }

Now, here there are two major steps. First we want to know if the item has been added or removed from the collection. If it’s added we create a new DockableContent and we set the Content to our view. Then we need to set a couple of properties like Title and name. In my case I am just adding the view, we will see later how to implement our TabModel property. What we can do then is to attach a listener to the close event of this tab. Why? Because when avalon will close a dock document we need also to destroy the corresponding view.

Then the second part is when the regionAdapter has a request of closing a view. We want to destroy the corresponding tab control.

Now go back a little bit and change the code in this way:

Code Snippet
  1. TabViewModel viewModel = ((UserControl)view).DataContext as TabViewModel;
  2. if (view != null)
  3. {
  4.     DockableContent newContentPane = new DockableContent();
  5.     newContentPane.Content = item;
  6.     if (viewModel != null)
  7.     {
  8.         Image img = new Image();
  9.         img.Source = new BitmapImage(new Uri(@"Resources/Alerts.png", UriKind.Relative));
  10.         newContentPane.Title = viewModel.TabModel.Title;
  11.         newContentPane.IsCloseable = viewModel.TabModel.CanClose;
  12.         newContentPane.Icon = img.Source;
  13.     }

It’s a little bit dirty but what we are trying to do here is to cast the View.DataContext to a type TabViewModel. It it’s the right type, as NET won’t throw an exception but simply returns an empty instance … we will populate the tab controls with our info.

The final result is this one:

SNAGHTML388d63e

The first can’t be closed and the second one can be, also we added a special icon to the context menu. More than that, this is still a WPF controls where you can apply your custom style. That’s it!

Ops, this is the new code in the MainView, of course:

XAML Avalon Dock adapter
  1. <ad:DockingManager Grid.Column="1" Grid.Row="1" >
  2.     <ad:DocumentPane cal:RegionManager.RegionName="TabRegion" Name="TabRegion">
  3.         <ad:DockableContent Title="Some title">
  4.  
  5.         </ad:DockableContent>
  6.     </ad:DocumentPane>
  7. </ad:DockingManager>

Final Note: As you can see building a custom region adapter is easy but you must know the control behind that, the one that will act as a region adapter. When you know that part you can play as you want. For example I have a region adapter for the ToolBar so every time you load a view, I pass the corresponding Toolbar to the main toolbar region. Same for the ribbon and for the Outlook bar.


Comments

  1. Gravatar matt dreid says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    you are awesome, that's it! thanks a tons!
  2. Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Could you please provide a sample project with the code in or a complete class file.

    Thank you.
    GE
  3. Gravatar raffaeu says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Sure I didn't have time yet as I am still working on it, next week-end I will publish a full region adapter for it and I will publish the code also on Avalon Dock project.
  4. Gravatar Shuhed says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    This is great stuff. As sample project of this will be very much appreciated.

    Thanks again.
  5. Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Cool, thank you. :D
  6. Gravatar TWebster says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    I've been trying the .NET 3.5 v1.3 build of AvalonDock and have had issues binding the Document.ItemsSource or DockingManager.DocumentSource to a collection of viewmodels, as can be done with TabControl in the MVVM pattern. I have also tried binding a collection of ManagedContent to these properties with no luck. Maybe this is what the RegionAdapter can accomplish, but I am not familiar with PRIM, so I do not know. How is IRegion region intended to be used in the OnViewsCollectionChanged method? It does not seem to get used there.

    I've been looking into the AvalonDock library and have noticed the developers are Italian like you :)
  7. Gravatar aaron says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Still waiting on completed prj...is it coming?
  8. Gravatar raffaeu says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    @aaron, yes it is coming, I just had a new dev in my team so no time for the blog this week, but do not worry.
    @TWebster well I don't see an easy way of working with WPF and MVVM without using Prism. Of course you need to build an attached behavior otherwise it will be pretty hard. AvalonDock has an Items collection, that is the place where you need to add panels ...
  9. Gravatar radian says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Very cool !!!
    Could you please provide a sample solution ?
  10. Gravatar Shane says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    This is very helpful. However None of the documents opened will close. What must be done to allow these to close?
  11. Gravatar Shekhar says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    this is awesome.. I'll try and use it in my project.
  12. Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Whole, working solution at
    http://www.yumasoft.com/node/35
  13. Gravatar raffaeu says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Great job!
  14. Gravatar Paul Reed says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Just curious...did you ever publish some sample code for the avalondock region adapter sample or is the link to www.yumasoft.com/node/35 the posting??????
  15. Gravatar Antonio says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Hi Raffaele,
    did you complete the AvalonDock Adapter?
    I did one following your example and it seems working.
    The issue I have is related to activate the DocumentPane for a specific view.
    I am using Prism v4 and the Region.NavigationService to move from one view to another. Using a simple TabControl it works fine, the view I want to navigate to become active, this is not happening with Avalon.
    I also tried the Region.Activate method but the result is the same.
    Do you have any hint where the problem lies?
    Thanks
    Antonio
  16. Gravatar raffaeu says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    xAntonio
    You have to implement the code that will select a specific View so that the Activate method will be honored by the Region Adapter. Remember if you implement your custom region adapter you have to implement the code to 'select' a specific View as every region container is different ... ;-)
  17. Gravatar Roman says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    This solution has a problem if you add content dynamically. I have a master detail view, where the details are DockableContents inside a DocumentPane. These details can be arranged freely with AvalonDock. The problem is, that AvalonDock is removing the DocumentPane sometimes if you make a DockableContent Floating. (Split DocumentContents horizontally and then make it floating)
    Now as the DocumentPane is gone, my PRISM region is gone too.

    Has anybody else this problem too?
  18. Gravatar Chris K says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    newContentPane.Closed += (contentPaneSender, args) =>
    {
    Debug.WriteLine("here");
    };

    Does not appear to get called. Any ideas?
  19. Gravatar raffaeu says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    I believe it depends on where you attach the listener to the event. It looks like more a problem with the avalon dock than not with the region adapter. On my side it works without any problem. I am also planning to post this definite region adapter for PRISM V4 ASAP
  20. Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    I had to add IsCloseable = true and HideOnClose = false ... otherwise avalondock doesn't actually close the tab, it just hides it and does not fire the Closed event
  21. Gravatar raffaeu says:

    Re : # re: WPf and Prism Tab Region Adapter, Part 02.

    Thanks Chris, I am planning to jump into PRISM V4 and review totally this RegionAdapter. Thanks again for the feedback, i will keep it in mind. In the post I just published partial of the code and probably I had to use that trick too but I do not remember right now.
Comments have been closed on this topic.