This project is read-only.

[Version 2.0] MVVM Support

Mar 2, 2012 at 7:20 PM

 

First of all, thanks for all the great effort!

 

Secondly, could there be a way to have multiple DocumentTemplates so that each DocumentTemplate ties to its own ViewModel?

That way we wouldn't need Caliburn to create documents with different appearances...

Thanks!

Steve

 

Mar 2, 2012 at 9:08 PM

Hum, even if I don't know Caliburn I think you could just use the DocumentTemplateSelector property to load a DocumentTemplate for each different model,

Ado

Mar 2, 2012 at 10:04 PM

Oh yes!! Brilliant, thanks!

Mar 3, 2012 at 11:05 PM

Ok,

I have my DocumentTemplateSelector working :)

The only problem with it is that when you switch document tabs, the control within the datatempate it is re-created which loses all its combo box selections :(

My way around this in the previous version of AvalonDock was to ditch the DataTemplates and use a concrete View control which never gets deleted/recreated.

Steve

Mar 4, 2012 at 12:07 AM

Hi Samneric,
Have a look at my thread http://avalondock.codeplex.com/discussions/343066
where I've been trying Caliburn Micro as well.
john

Mar 4, 2012 at 1:05 AM

Cheers John,

but if this DataTemplate approach is all thats available, I'm going to stick with my customized version of this solution....

http://msdn.microsoft.com/en-us/magazine/ff798279.aspx

Steve

Mar 4, 2012 at 1:29 PM

Hi Steve,
That's a very interesting article that I'd not seen before, but now need to read a few more times!!
Your comments to it are equally very interesting.
Have you (will you) Blog about your solution, and post an excerpt of your code?

Thanks for the article link and mentioning it.
John

Mar 4, 2012 at 6:32 PM

Hi Steve,
I think that your problem is common to all scenarios that involve the use of multi-document views (AvalonDock but also the standard TabControl). Your solution could work but in this case I would try to avoid as much as possible attaching a control to the model layer.

For example in your case I'd bind combo box SelectedItem property to a model property that will store combos selection in TwoWay mode. Doing so everytime a tab is activated combos selection are restored where previously saved.

Ado

Mar 4, 2012 at 7:13 PM

Sure but with the volume of controls on my document it was crucifying performance - especially with the selected item bindings refreshing.

I presently do create the View control when the Document is first created - I know this breaks MVVMs principles but the model doesn't retain a reference to the View and testing still works which is the main reason I use MVVM. Also, I *could* pass the Type of the View control to the DocumentManagerAdapter or devise my own way of creating a View control based on the type of the ViewModel but I had so much work to do on my project (I was learning WPF, MVVM and Entity Framework while still working to a deadline) that I just left it as is.

I may go back and devise a mechanism to create the View objects from a resource dictionary and also write a blog post (thanks John!) on the subject when work finally calms down.

P.S. John -  I wasn't using all the Dynamic property stuff that Robert was describing - only the Document/DocumentManager/DocumentManagerAdapter design pattern.

Ado - thanks again for the great effort - your code is well written - I had to dive into it a few times to customize certain aspects (I love open source).

 

Mar 5, 2012 at 7:10 PM

I'm going to try the DataTemplate approach now that I have more time to play around with it (and not under project deadline pressure to "make it work anyway possible").

As I remember, when using v1.3 and I switched tabs, the combobox selecteditems were refreshing ok but it was the DataGrid selecteditems that I couldn't get to refresh. Now that our company purchased DevExpress controls, I'm using their GridControl which has a solution to the selecteditems binding problem (http://www.devexpress.com/Support/Center/p/B190790.aspx) that isn't workable in DataGrid from Microsoft.

So, with that in place I can allow the controls to re-generate when tabs are changed, knowing that the selections will remain intact.

I think the performance issues I came across were experienced while running my project from the Visual Studio debugger. I will try again with a stand-alone executable.

Steve

Mar 29, 2012 at 8:11 PM

Hi Samneric,

I have been thinking about how to overcome the problem of controls losing state information when the ActiveDocument changes. I am using DataTemplates for my ViewModels and my own extended version of the WPF DataGrid with various extra features and these lose state each time the ActiveDocument changes. Please could you expand on how you solved this problem?

Thanks

Mar 29, 2012 at 8:51 PM

I haven't gotten that far with version 2 yet.

Although I am going to deal with the WPF Grid problem by ignoring it :)

I am using DevExpress's grid that allows you to bind the user's selections to a property on the viewmodel.

With the standard Windows DataGrid you will have to write a behaviour that keeps track of the user's selections and re-selects them the cumbersome way (looping through each row and re-selecting it if it was selected when the control lost focus) when the control hosting the Grid is re-activated.

I hope this makes sense as I am just explaining what I WOULD have done if not using DevExpress's control. 

Steve

 

Mar 29, 2012 at 10:25 PM

I wonder whether this is of any help.
True, it's Caliburn.Micro focused, but the demo shows a dramatic performance improvement.

http://www.baud.cz/blog/fast-switching-between-viewmodels-in-caliburn.micro

John

Mar 30, 2012 at 2:28 PM

Yuppers - caching the Views like that would work.

I mentioned above the solution I came up with to overcome the performance issues when switching tabs.

I set the DocumentContent.Content property to a concrete View rather than a ViewModel (which was being rendered every activation by a WPF DataTemplate).

My DocumentManager/DocumentManagerAdapters take care of the document switching - the DocumentContent.Content never changes so a new View isn't created hence I get maximum performance. As ADO mentioned - my initial approach does break the MVVM pattern by having an initial reference to the View in my ViewModels but that reference is removed once the View is assigned to the DocumentContent.Content property.

I will probably design some form of View injection into Version 2.0 of AvalonDock because I'm sure I won't be happy with the performance hit of re-creating my View controls every time the ActiveContent changes when using DataTemplateSelectors.

Steve

Apr 4, 2012 at 8:44 PM

Hi samneric and jradxl,

 

Thanks for your suggestions. I had just started thinking it through a bit further myself when I read your posts. Now that I have a break from real work for a few days I have been able to complete my first pass at my own "ViewManager" which caches the views. Basically my "ViewManager" acts as a bridge between the View and ViewModel by binding to both. The ViewManager resides in the View project and is instantiated in App.xaml. The MainViewModel holds a collection of "Workspace" objects and this collection is bound to a collection in the ViewManger. The ViewManager creates the views when ViewModels are added to the collection etc. The ViewManager also has a WorkspaceView property which is then bound to AvalonDock.

It seems to work!

 

Apr 4, 2012 at 9:17 PM

Hi ChangedDaily
Sounds very interesting....
I hope you're going to Blog about your solution.

John

Apr 6, 2012 at 12:42 PM

Hi, please see my ViewManager class below. I expect some bits will change as my project moves on...

 

public class ViewManager : DependencyObject, INotifyCollectionChanged
    {
        #region Constructor

        public ViewManager()
        {
            WorkspaceCloseCommand = new DelegateCommand<object>((param) => CloseWorkspace(param));
        }

        #endregion Constructor

        #region Workspaces property

        /// <summary>
        /// Used to bind to ViewModel Workspace collection.
        /// Two-way binding is used to keep the collections in sync.
        /// </summary>
        public static readonly DependencyProperty WorkspacesProperty =
            DependencyProperty.Register("Workspaces", typeof(ObservableCollection<WorkspaceViewModel>),
            typeof(ViewManager), new FrameworkPropertyMetadata(new ObservableCollection<WorkspaceViewModel>(),
            OnWorkspacesChanged));

        public ObservableCollection<WorkspaceViewModel> Workspaces
        {
            get
            {
                return (ObservableCollection<WorkspaceViewModel>)GetValue(WorkspacesProperty);
            }
            set
            {
                SetValue(WorkspacesProperty, (ObservableCollection<WorkspaceViewModel>)value);
            }
        }

        public static void OnWorkspacesChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            ViewManager manager = source as ViewManager;

            if (manager != null)
            {
                manager.OnWorkspacesChanged(e.OldValue, e.NewValue);
            }
        }

        /* Not needed at the moment. */
        public void OnWorkspacesChanged(object oldValue, object newValue)
        {
            //ObservableCollection<WorkspaceViewModel> o = oldValue as ObservableCollection<WorkspaceViewModel>;
            //ObservableCollection<WorkspaceViewModel> n = newValue as ObservableCollection<WorkspaceViewModel>;

            //if (o != null)
            //{
            //    o.CollectionChanged -= OnWorkspaceCollectionChanged;
            //}

            //if (n != null)
            //{
            //    n.CollectionChanged += OnWorkspaceCollectionChanged;
            //}

            //SetValue(WorkspacesProperty, n);
        }
        private void OnWorkspaceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            //if (e.Action == NotifyCollectionChangedAction.Reset)
            //{
                
            //}

            //if (e.NewItems != null)
            //{
            //    ObservableCollection<WorkspaceViewModel> collection =
            //        new ObservableCollection<WorkspaceViewModel>();

            //    foreach (WorkspaceViewModel vm in e.NewItems)
            //    {
            //        collection.Add(vm);
            //    }

            //    SetValue(WorkspacesProperty, collection);
            //}

            //if (e.OldItems != null)
            //{
            //    ObservableCollection<WorkspaceViewModel> collection =
            //        (ObservableCollection<WorkspaceViewModel>)GetValue(WorkspacesProperty);

            //    foreach (WorkspaceViewModel vm in e.OldItems)
            //    {
            //        collection.Remove(vm);
            //    }

            //    SetValue(WorkspacesProperty, collection);
            //}

            //SetValue(WorkspacesProperty, _workspaces);
        }
        public static object OnCoerceWorkspaces(DependencyObject source, object value)
        {
            return value;
        }

        #endregion Workspaces property

        #region ActiveWorkspace property

        /// <summary>
        /// 
        /// </summary>
        public static readonly DependencyProperty ActiveWorkspaceProperty =
            DependencyProperty.Register("ActiveWorkspace", typeof(object), typeof(ViewManager),
                new FrameworkPropertyMetadata(OnActiveWorkspaceChanged, OnCoerceActiveWorkspace));

        public object ActiveWorkspace
        {
            get
            {
                return GetValue(ActiveWorkspaceProperty);
            }
            set
            {
                SetValue(ActiveWorkspaceProperty, value);
            }
        }

        public static void OnActiveWorkspaceChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            ViewManager manager = source as ViewManager;

            if (manager != null)
            {
                manager.OnActiveWorkspaceChanged(e.OldValue, e.NewValue);
            }
        }

        public static object OnCoerceActiveWorkspace(DependencyObject source, object value)
        {
            return value;
        }

        public void OnActiveWorkspaceChanged(object oldValue, object newValue)
        {
            ContentControl control = new ContentControl();
            control.Content = newValue;
            control.ContentTemplate = (DataTemplate)App.Current.Resources[newValue.GetType().Name];
            WorkspaceViews.Add(control);
            ActiveWorkspaceView = control;
        }

        #endregion ActiveWorkspace property

        #region WorkspaceViews property

        /// <summary>
        /// Collection of Views which is intended to be bound to AvalonDock (but not necessarily...)
        /// </summary>
        public static readonly DependencyProperty WorkspaceViewsProperty =
            DependencyProperty.Register("WorkspaceViews",
            typeof(ObservableCollection<object>),
            typeof(ViewManager),
            new PropertyMetadata(new ObservableCollection<object>()));

        public ObservableCollection<object> WorkspaceViews
        {
            get
            {
                return (ObservableCollection<object>)GetValue(WorkspaceViewsProperty);
            }
            set
            {
                SetValue(WorkspaceViewsProperty, (ObservableCollection<object>)value);
            }
        }

        #endregion WorkspaceViews property

        #region ActiveWorkspaceView property

        /// <summary>
        /// ActiveWorkspace object which is bound to AvalonDock "ActiveContent".
        /// </summary>
        public static readonly DependencyProperty ActiveWorkspaceViewProperty =
            DependencyProperty.Register("ActiveWorkspaceView", typeof(object), typeof(ViewManager),
            new FrameworkPropertyMetadata(OnActiveWorkspaceViewChanged, OnCoerceActiveWorkspaceView));

        public object ActiveWorkspaceView
        {
            get
            {
                return GetValue(ActiveWorkspaceViewProperty);
            }
            set
            {
                SetValue(ActiveWorkspaceViewProperty, value);
            }
        }

        /* Not needed at the moment. */
        public static object OnCoerceActiveWorkspaceView(DependencyObject source, object value)
        {
            return value;
        }
        public static void OnActiveWorkspaceViewChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
        {
            ViewManager manager = source as ViewManager;

            if (manager != null)
            {
                manager.OnActiveWorkspaceViewChanged(e.OldValue, e.NewValue);
            }
        }
        public void OnActiveWorkspaceViewChanged(object oldValue, object newValue)
        {
        }

        #endregion ActiveWorkspaceView property

        #region CloseWorkspace

        public DelegateCommand<object> WorkspaceCloseCommand
        {
            get;
            private set;
        }

        /* Deal with this here. Alternatively the event could be forwarded to the ViewModel.
         * Or possibly change binding Source in xaml to bind directly to ViewModel command.
         * This may be necessary for example if there are unsaved changes in data... */
        public void CloseWorkspace(object workspaceView)
        {
            LayoutContent content = workspaceView as LayoutContent;
            ContentControl control = content.Content as ContentControl;
            WorkspaceViewModel viewModel = control.Content as WorkspaceViewModel;

            WorkspaceViews.Remove(control);
            Workspaces.Remove(viewModel);
        }

        #endregion CloseWorkspace

        #region INotifyCollectionChanged

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        #endregion INotifyCollectionChanged
    }

 

Please feel free to let me know how to make it better!

 

Aug 24, 2012 at 7:18 PM

I'm trying to use your ViewManager but I have no idea how, can you please help me?

Aug 30, 2012 at 12:51 PM

Hi,

Sorry for the delay in replying, I have been away with no access to any electronic communications at all!

You need to create an instance of the ViewManager class, probably in App.xaml or your mainWindow.xaml.

You then bind your viewmodels to the "Workspaces" property. The viewmodel must have a datatemplate defined at application level. The ViewManager will then create the view and add to the "WorkspaceViews" collection. You should bind the WorkspaceViews collection to AvalonDock Documents property.

 

Sep 10, 2012 at 7:38 PM

I tried but couldn't make it work.

The Proof Of Concept project is at http://j.mp/avalonDock_MVVM

I saw that when I bind the avalonDock:DockingManager DocumentsSource to Workspaces, I see the tabs but the getter of the property gets hit every time I change the active document.

If I bind it to WorkspaceViews, I don't get any tab in the avalon dock.

 

Thank you for your time.