Docking and MVVM

1 posts, 0 answers
  1. Frank
    Frank avatar
    1 posts
    Member since:
    Apr 2016

    Posted 05 May Link to this post

    Requirements

    Telerik Product and Version

    UI WPF Q1 2016

    Supported Browsers and Platforms

    Components/Widgets used (JS frameworks, etc.)



    PROJECT DESCRIPTION 

    One of the major issues I've encountered in Telerik's docking controls is the lack of true MVVM support for pane content. I've found a couple of possible solutions on the Internet but either they are not true MVVM or they are not integrating smoothly with Telerik's pane creation solution (aka docking panes factory).

    A frequently proposed solution featuring view model binding is to have the DockingPanesFactory create the RadPane content by instantianting a class type provided by the content view model. That's not what I'd call true MVVM because view models need to know about view class types. At first glance, it might look acceptable, but what if you want to test the view model in a view-less environment? You'd have to write view stubs and make them somehow replace the real ones. This adds a level of structural complexity I'd definitely want to avoid in bigger projects. Some might argue, I could use strings to denote the view. But then I'm forced to implement some kind of mapping and object creation in a view factory. In the presence of a concept as beautiful as data templates and data template selection, I consider re-implementing this concept by something as crude as a string-based view factory a confession of failure.

    Another solution I've found implements true MVVM on the RadPaneGroup (including pane data templates). This solution, however, re-implements binding to a pane view model collection by exposing a local PanesSource property on RadPaneGroup. Additionally, the RadPaneGroups have to be statically defined in XAML for the PanesSource property to be easily bindable to the pane view model collection. Dynamically creating a RadPaneGroup requires programmatic binding to the pane view model collection.

    That's what I'd expect from a conceptually well-designed solution:

    1. The pane view model collection shall be exposed by the docking root view model and RadDocking's PanesSource property shall be used to bind to the pane view model collection. DockingPanesFactory shall be used for pane creation so that whatever happens behind the scenes in the collaboration of RadDocking and DockingPanesFactory is not annulled by the extensions.
    2. The pane content (not the pane itself) shall be creatable from a data template. The pane content data template shall be either a fixed one or selectable according to view model type and/or property values. A docking control property (say PaneContentTemplate) shall receive the fixed pane content data template. Another docking control property (say PaneContentTemplateSelector) shall receive the pane content data template selector.
    3. The pane style shall be either a fixed one or selectable according to view model type and/or property values. A docking control property (say PaneStyle) shall receive the fixed pane style selector. Another docking control property (say PaneStyleSelector) shall receive the pane style selector.
    4. No view objects, view types, strings denoting view types and the like in view model.

    The following steps sketch the solution's main aspects. The provided zip archive contains the complete source code of a functional sample application.

    Step 1:

    Create a class (say MvvmDocking) derived from RadDocking and implement dependency properties PaneStyle, PaneStyleSelector, PaneContentTemplate and PaneContentTemplateSelector.

     

    public class MvvmDocking : RadDocking
    {
        public static readonly DependencyProperty PaneStyleProperty = DependencyProperty.Register
            ("PaneStyle", typeof (Style), typeof (MvvmDocking), new PropertyMetadata (default (Style)));
     
        public Style PaneStyle
        {
            get { return ((Style)GetValue (PaneStyleProperty)); }
            set { SetValue (PaneStyleProperty, value); }
        }
     
     
        public static readonly DependencyProperty PaneStyleSelectorProperty = DependencyProperty.Register
            ("PaneStyleSelector", typeof (StyleSelector), typeof (MvvmDocking),
            new PropertyMetadata (default (StyleSelector)));
     
        public StyleSelector PaneStyleSelector
        {
            get { return ((StyleSelector)GetValue (PaneStyleSelectorProperty)); }
            set { SetValue (PaneStyleSelectorProperty, value); }
        }
     
     
        public static readonly DependencyProperty PaneContentTemplateProperty = DependencyProperty.Register
            ("PaneContentTemplate", typeof (DataTemplate), typeof (MvvmDocking),
            new PropertyMetadata (default (DataTemplate)));
     
        public DataTemplate PaneContentTemplate
        {
            get { return ((DataTemplate)GetValue (PaneContentTemplateProperty)); }
            set { SetValue (PaneContentTemplateProperty, value); }
        }
     
     
        public static readonly DependencyProperty PaneContentTemplateSelectorProperty = DependencyProperty.Register
            ("PaneContentTemplateSelector", typeof (DataTemplateSelector), typeof (MvvmDocking),
            new PropertyMetadata (default (DataTemplateSelector)));
     
        public DataTemplateSelector PaneContentTemplateSelector
        {
            get { return ((DataTemplateSelector)GetValue (PaneContentTemplateSelectorProperty)); }
            set { SetValue (PaneContentTemplateSelectorProperty, value); }
        }
    };

     

    Step 2:

    Add methods to MvvmDocking for determining the pane style and the pane content data template selector. They are internal because they will be called by our DockingPanesFactory extension.

     

    internal Style GetStyle (object item)
    {
        StyleSelector selector;
     
        selector = PaneStyleSelector;
        if (selector != null)
        {
            Style style;
     
            style = selector.SelectStyle (item, this);
            if (style != null)
                return (style);
        };
     
        return (PaneStyle);
    }
     
     
    internal DataTemplate GetDataTemplate (object item)
    {
        DataTemplateSelector selector;
     
        selector = PaneContentTemplateSelector;
        if (selector != null)
        {
            DataTemplate dataTemplate;
     
            dataTemplate = selector.SelectTemplate (item, this);
            if (dataTemplate != null)
                return (dataTemplate);
        };
     
        return (PaneContentTemplate);
    }

     

    Step 3:

    Create a class (say MvvmDockingPanesFactory) derived from DockingPanesFactory. Override CreateItemPane for creating the appropriate pane object and pane content object according to its view model (some MvvmPaneModel class). Note that in this implementation the pane object can be either RadPane or RadDocumentPane depending on the view model's IsDocument property. If you need, you can certainly extend that to having some creator object create the appropriate pane object depending on view model type and/or properties. This creator object can be associated with the docking object in the same way style selector and data template are.

     

    public class MvvmDockingPanesFactory : DockingPanesFactory
    {
        protected override RadPane CreatePaneForItem (RadDocking docking, object item)
        {
            PaneModel paneModel;
            Style style;
            DataTemplate dataTemplate;
            RadPane pane;
     
            if (!(docking is MvvmDocking))
                return (base.CreatePaneForItem (docking, item));
     
            paneModel = item as PaneModel;
            if (paneModel == null)
                return (base.CreatePaneForItem (docking, item));
     
            style = ((MvvmDocking)docking).GetStyle (item);
            dataTemplate = ((MvvmDocking)docking).GetDataTemplate (item);
     
            if (paneModel.IsDocument)
                pane = new RadDocumentPane ();
            else
                pane = new RadPane ();
     
            if (style != null)
                pane.Style = style;
     
            pane.Content = dataTemplate.LoadContent ();
     
            pane.DataContext = item;
            return (pane);
        }
    }

     

    Step 4:

    Create the pane style selector class (say PaneStyleSelector) and pane content data template selector class (say PaneContentTemplateSelector). The interface exposed by style selectors and data template selectors is well-defined by WPF. The actual implementation depends on your needs. The sample code provided and the XAML snippet below should give you a sufficiently good conception on how it works.

     

    <local:MvvmDocking PanesSource="{Binding Panes}">
     
        <!-- Docking Panes Factory -->
        <telerik:RadDocking.DockingPanesFactory>
            <local:MvvmDockingPanesFactory/>
        </telerik:RadDocking.DockingPanesFactory>
     
        <!-- Pane Style Selector -->
        <local:MvvmDocking.PaneStyleSelector>
            <local:PaneStyleSelector>
     
                <local:PaneStyleSelector.DocumentStyle>
                    <Style TargetType="{x:Type telerik:RadPane}">
                        <!-- define style here -->
                    </Style>
                </local:PaneStyleSelector.DocumentStyle>
     
                <local:PaneStyleSelector.ToolStyle>
                    <Style TargetType="{x:Type telerik:RadPane}">
                        <!-- define style here -->
                    </Style>
                </local:PaneStyleSelector.ToolStyle>
     
            </local:PaneStyleSelector>
        </local:MvvmDocking.PaneStyleSelector>
     
        <!-- Pane Content Template Selector -->
        <local:MvvmDocking.PaneContentTemplateSelector>
            <local:PaneContentTemplateSelector>
     
                <local:PaneContentTemplateSelector.DocumentTemplate1>
                    <DataTemplate>
                        <!-- define data template here -->
                    </DataTemplate>
                </local:PaneContentTemplateSelector.DocumentTemplate1>
     
                <local:PaneContentTemplateSelector.DocumentTemplate2>
                    <DataTemplate>
                        <!-- define data template here -->
                    </DataTemplate>
                </local:PaneContentTemplateSelector.DocumentTemplate2>
     
                <local:PaneContentTemplateSelector.ToolTemplate1>
                    <DataTemplate>
                        <!-- define data template here -->
                    </DataTemplate>
                </local:PaneContentTemplateSelector.ToolTemplate1>
     
                <local:PaneContentTemplateSelector.ToolTemplate2>
                    <DataTemplate>
                        <!-- define data template here -->
                    </DataTemplate>
                </local:PaneContentTemplateSelector.ToolTemplate2>
     
            </local:PaneContentTemplateSelector>
        </local:MvvmDocking.PaneContentTemplateSelector>
     
    </local:MvvmDocking>

     

    The provided sample application allows for adding two kinds of document windows and two kinds of tool windows using some very basic data template content. The respective data template is selected by the pane content data template selector according to the pane content view model type. You can change the selection strategy to whatever you need. The respective pane style is selected by the pane style selector according to the IsDocument property value of the pane content view model. Again, you can adapt that to your specific needs.

Back to Top