Catel and TreeView example

Topics: Questions
Oct 17, 2012 at 7:30 PM

Is there an example of how to use Catel with the default WPF Treeview?

Should i create a custom TreeViewItem that implements IUserControl?

Coordinator
Oct 18, 2012 at 7:08 AM

Hi,

You shouldn't create a custom VM per treeviewitem. There is no real example yet, though I wrote a tree view myself quite some times. An open-source version is available here. Also check out the ViewModels folder which contains the view model.

I will write a blog post soon, but currently very busy.

Oct 18, 2012 at 8:21 PM

Ok this code confuses me.

If I read the code correct the project model is wrapped in a ObservableObject(SelectedProject) and in order to avoid the nested user control problem the object grap is 'flattened'. Feels awkward to me.

I think i would want to follow the conventional approach (wrap model with ViewModelBase)

 

    public class HotelViewModel : TreeViewItemViewModel
    {
        public HotelViewModel(Hotel hotel)
        {
            Hotel = hotel;
        }

        public static readonly PropertyData HotelProperty = RegisterProperty("Hotel", typeof(Hotel));
        [Model]
        public Hotel Hotel
        {
            get { return GetValue<Hotel>(HotelProperty); }
            set { SetValue(HotelProperty, value); }
        }

        public static readonly PropertyData DescriptionProperty = RegisterProperty("Description", typeof(string));
        [ViewModelToModel("Hotel")]
        public string Description
        {
            get { return GetValue<string>(DescriptionProperty); }
            set { SetValue(DescriptionProperty, value); }
        }

        public static readonly PropertyData RoomsProperty = RegisterProperty("Rooms", typeof(ICollection<Room>));
        [ViewModelToModel("Hotel")]
        public ICollection<Room> Rooms
        {
            get { return GetValue<ICollection<Room>>(RoomsProperty); }
            set { SetValue(RoomsProperty, value); }
        }
    }

    public class RoomViewModel : TreeViewItemViewModel
    {               
        public RoomViewModel(Room room)
        {
            Room = room;
        }

        public static readonly PropertyData RoomProperty = RegisterProperty("Room", typeof(Room));
        [Model]
        public Room Room
        {
            get { return GetValue<Room>(RoomProperty); }
            set { SetValue(RoomProperty, value); }
        }

        public static readonly PropertyData DescriptionProperty = RegisterProperty("Description", typeof(string));
        [ViewModelToModel("Room")]
        public string Description
        {
            get { return GetValue<string>(DescriptionProperty); }
            set { SetValue(DescriptionProperty, value); }
        }
    }
 
Create an 
ObservableCollection<HotelViewModel> HotelViewModels
 

<TreeView ItemsSource="{Binding HotelViewModels}" Grid.Row ="1">                   
            <TreeView.Resources>
                <HierarchicalDataTemplate 
                    DataType="{x:Type ViewModels:HotelViewModel}"
                    ItemsSource="{Binding Rooms}">
                    <Views:HotelUserControl DataContext="{Binding}" />
                </HierarchicalDataTemplate>
                <HierarchicalDataTemplate 
                    DataType="{x:Type ViewModels:RoomViewModel}">
                    <Views:RoomUserControl DataContext="{Binding}" />
                </HierarchicalDataTemplate>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
                    <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />                    
                </Style>                           
            </TreeView.Resources>          
        </TreeView>
Now the Rooms are not displayed since it is a ICollection<Room> instead of a ICollection<RoomViewModel>
Somehow the HotelViewModel needs an ICollection<RoomViewModel> but then I basically end up creating a duplicate viewmodel object graph.
I could write a sync class that keeps both graphs in sync, but that does not seem right either, (Catel does this behind the sheets already).  
Oct 24, 2012 at 6:48 PM

Creating a Collection of ViewModels is not the way to go.

I've created a sample app with a treeview using the viewmodels showns above.

The treeview's itemsource is set to an observablecollection<Hotel>, so it's bound to the model. Catel does it's job and creates viewmodels for the usercontrols.

All good so far.

To get multiselect working i'd prefer using the method descibed here (Josh Smith):

 

http://www.codeproject.com/Articles/26288/Simplifying-the-WPF-TreeView-by-Using-the-ViewMode

The only thing i can't get to work is the part where i want to bind IsSelected and IsExpanded of a TreeViewItem to the ViewModel since i am binding to the model pieces.

So this wont work:



<Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
                    <Setter Property="IsExpanded" Value="{Binding  IsExpanded, Mode=TwoWay}" />
                </Style>

Not sure how to bind these properties to the viewmodel from here (tried VisualTreeHelper).

Any suggestions?

If somebody wants to have a look at the code let me know.

 

Coordinator
Oct 24, 2012 at 6:53 PM

If you can put the code on dropbox (or something like that) so we can download it, we can take a look at it.

Oct 24, 2012 at 10:06 PM

https://skydrive.live.com/redir?resid=8E285F74252F9EAF!443&authkey=!AI7joon5xZLKJ8Q
Sent from my Verizon Wireless Phone

Coordinator
Oct 26, 2012 at 6:59 PM

Thanks. Sorry for my late reply, but I was working very hard on WinRT support (project and item templates, getting the libraries into the automatic build, etc). I will look into this asap.

Coordinator
Oct 27, 2012 at 11:40 AM

I tried to find a good solution, but I can't seem to find it. I tried a converter that subscribes to the loaded state, but then you cannot bind twoway. If you want, you might get new ideas out of this code:

    public class TreeViewItemToDataContextConverter : ValueConverterBase
    {
        protected override object Convert(object value, Type targetType, object parameter)
        {
            var treeViewItem = value as TreeViewItem;
            if (treeViewItem != null)
            {
                if (!treeViewItem.IsLoaded)
                {
                    treeViewItem.Loaded += OnLoaded;
                    return false;
                }

                var viewModelContainer =
                    treeViewItem.FindVisualDescendant(x => x is IViewModelContainer) as IViewModelContainer;
                if (viewModelContainer != null)
                {
                    var vm = viewModelContainer.ViewModel as TreeViewItemViewModel;
                    if (vm != null)
                    {
                        vm.IsSelected = treeViewItem.IsSelected;
                    }
                }
            }

            return false;
        }

        protected override object ConvertBack(object value, Type targetType, object parameter)
        {
            return base.ConvertBack(value, targetType, parameter);
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            var item = (TreeViewItem) sender;

            item.Loaded -= OnLoaded;

            BindingExpression bindingExpression = item.GetBindingExpression(TreeViewItem.IsSelectedProperty);
            if (bindingExpression != null)
            {
                bindingExpression.UpdateTarget();
            }
        }
    }

Oct 29, 2012 at 2:45 PM

Hi Geert,

Thanks for looking into this. I found a solution by setting the DataContext of the TreeViewitem to the viewmodel when the UserControl is loaded

/// <summary>
    ///// Interaction logic for HotelUserControl.xaml
    /// </summary>
    public partial class HotelUserControl
    {
        public HotelUserControl()
        {
            InitializeComponent();
        }

        protected override void OnLoaded(System.EventArgs e)
        {
            base.OnLoaded(e);           
            var visualParent = this.VisualParent as ContentPresenter;
            if (visualParent != null)
            {
                var treeViewItem = visualParent.TemplatedParent as TreeViewItem;
                if (treeViewItem != null)
                {
                    treeViewItem.DataContext = this.ViewModel;
                }
            }
        }
    }

It will however create a bunch of BindingExpression path errors on initialization until the code above is executed.

There might be other situations where the DataContext has to be (re)bound to the viewmodel (I am thinking docking control) and i wonder if something like the code above could be integrated into a TreeViewItemUserControl: Catel.UserControl for example.

Oct 29, 2012 at 7:55 PM

Ok, now things get interesting. In order to get the selected treeview items Josh suggests to check all the ViewModels and check the IsSelected property. (This method can also be used to make the treeview multiselect)

However since with Catel the ViewModels are dynamic this won't be possible. I guess i should make a seperate list to keep track of selected items.

 

Oct 29, 2012 at 8:18 PM

Hi Seaducer,

Your selection issue is not limited to treeview. I have similar issues when dealing with lists of model objects that are displayed in a Catel View, because the View automatically creates a VM from the model.

In my opinion, keeping track of the dynamically created VMs is not worth the effort.

I decided to move the 'IsSelected' property to the model and simply have the View expose the model property.

Oct 29, 2012 at 10:32 PM

Hmm, what about IsExpanded, IsGrouping, IsFocused etc.? I don't think these should be moved to the model, they should all be part of the ViewModel.

I was thinking to keep a list of models for the selected items in the main viewmodel. 

Right now I am running into another issue as well. I would really prefer not to reference the model directly from view xaml. The default template selector does not support interfaces, so i might have to do something with a DataTemplateSelector as described here

http://stackoverflow.com/questions/259063/wpf-hiercharchicaldatatemplate-datatype-how-to-react-on-interfaces
Oct 29, 2012 at 10:39 PM

ok, perhaps what you want is a 'Model Wrapper' which adds the properaties (IsExpanded, etc) to your lower level model. The VM contains a wrapper as its model.

Then your code can return a list of thes wrappers. The wrapper can handle the interface issues.

Oct 30, 2012 at 7:30 PM

I have updated the example and included:

- DataTemplateSelector: The model is now completely behind an interface (The View and ViewModels have no references to the model)

- TreeViewItem properties like IsSelected and IsExpanded are bound to viewmodel properties using a workaround.

-SelectedItem is tracked in the viewmodel.

However I continue to run into issues (tracking selected items for example), when the treeview's itemsource is bound to a collection of models and not viewmodels. The same issues would arise for any other ItemsSource (Listbox for example) which makes me doubt the approach and even the use of Catel at times. 

 https://skydrive.live.com/redir?resid=8E285F74252F9EAF!443&authkey=!AI7joon5xZLKJ8Q

Martin

Oct 30, 2012 at 8:17 PM

I don't have time to look at your code atm...

I use Catel with lists of Model objects.

Because of the dynamic nature of the VM creation your code will end up comparing copies of objects. Conseqently all my Model classes (ie the classes with the Model tag in the VM) have the follwing methods (each class has its own implementation). These ensure that your 'equality' comparisons will give the desired results

 

        public string HashCodeStr() { return string.Format("{0}{1}{2}{3}", ClientId, WorkDate, TimeIn, TimeOut); }
        public override int GetHashCode()
        {
            var result = 0;
            if (!string.IsNullOrEmpty(Id))
            {
                result = Id.GetHashCode();
            }
            result = HashCodeStr().GetHashCode();
            return result;
        }
        bool Equals(WorkRecordEdit other)
        {
            var result = RefEquals(other);
            if (result == null)
            {
                result = GetHashCode() == other.GetHashCode();
            }
            return result.Value;
        }
        public static bool Equals(WorkRecordEdit a_, WorkRecordEdit b_)
        {
            if (Object.ReferenceEquals(a_, null))
            {
                return false;
            }
            var result = false;
            result = a_.Equals(b_);
            return result;
        }

Coordinator
Oct 30, 2012 at 8:18 PM

Ok, just a few tips:

1) The ModelBase in Catel takes care of this for your

2) Use ObjectHelper.AreEqual to check for ReferencesEquals as well

Oct 30, 2012 at 8:31 PM

Geert,

my models are CSLA objects and can't derive from Catel Modelbase. So I add the equality methods.

Oct 30, 2012 at 9:46 PM

I'm not sure where the last comments fit in with the treeview (ItemsSource) discussion.

I think the nature of having dynamic viewmodels has pros, but i am running into some roadblocks when it comes to binding to collections (ItemsSource).

- What if I add a property X to the Viewmodel and i would like to filter, sort or group for that property? 

- What if I need to set focus on an item in a collection that is not yet in view?

- Or with the treeview example above (ViewModel with an IsSelected property), how can I select if the viewmodel is not yet initialized

Oct 31, 2012 at 8:14 PM

I havent used TreeViews in my Catel apps so far, so I am not commenting directly on them.

However my comments re wrapper classes and equality methods apply to any collection to which you want a control to bind.

If you want selection, sorting, filtering etc you need to provide appropriate properties in the objects in the bound list. So if you have a list of model objects, the model class must have the properties. If you use a model wrapper class, the wrapper must have the properties. If you want to have the properties in the VM, you need to build & bind to a list of VM objects.

Its your application, so you need to decide what is the appropriate type of object to be in the list.

In my case, when binding to listviews and/or datagrids I tend to use lists of model or wrapper objects as it is typically 'cheaper' to construct them.

Coordinator
Oct 31, 2012 at 9:34 PM

Ok, I was busy, but here is a longer answer.

1) Dynamic view models are the default behavior. However, you can have complete control over view model instantiation. You can do this per control (override the GetViewModelInstance method). However, you can also create your own implementation of the IViewModelFactory and take full control and re-use.

To sum up: dynamic view models are super cool, but you are not forced to use that mechanism.

2) What if I need to set focus on an item in a collection that is not yet in view?

You will need to write a custom behavior somehow to expand it all. You can create this in the code-behind or create a generic behavior (think of blend behaviors).

3) how can I select if the viewmodel is not yet initialized

You should make it generic. In most treeviews, you can expand by path (/parent/child/subchild). If you expand this one by one, the vms will be created on the fly and you should be able to do anything you want.

Nov 2, 2012 at 2:10 PM

I think dynamic viewmodels are supercool too :) Catel seems to be the only framework out there that is even thinking about viewmodels to extend models.

The GetViewModelInstance method only gets called when the usercontrol is created, so overriding it won't help a lot. I'd like to have all viewmodels ready before any usercontrol is displayed. Could an implementation of IViewModelFactory help me there?

Expanding a treeview all the way to the one you want to see is not very user friendly. The viewmodel should be able to bind to specific control properties like the TreeViewItem.IsSelected, ListBoxItem.IsFocused or ThirdPartyControlX.SpecificPropertyY.

Perhaps the GetViewModel method in IViewModelManager could help me out there. I'm not sure where the uniqueId comes from though. 

Coordinator
Nov 2, 2012 at 4:54 PM

In that case, you can do your own view model management. Note though that this can become very complex.

What you can do is implement your own view model factory. As soon as you see that the tree view model is loaded, you can instantiate all the view models in memory and keep them in a custom container. Then, when the view model factory requests a view model instantiation, you can pass the already created one.

Then you can close them all when the main tree view is closed. This is a bit complex, but then you got exactly what you want. I think the IViewModelFactory is the interface you will need to implement to make this work.

Nov 6, 2012 at 10:04 PM

Nice job on the beta!

I've created an ObservableViewModelCollection that basically wraps any model object graph into a viewmodel object graph.

I'm wondering about the ParentViewModel. the IViewModelFactory does not have a way to get any parent viewmodel in. How can i set the ParentViewModel property(can i use SetParent?) and would setting the ParentViewModel be enough to get some of the nested user control pieces (like validation) to work?

Coordinator
Nov 7, 2012 at 10:14 AM

Parent view models are handled using the IRelationalViewModel. All methods are implemented explicitly to prevent abuse / misuse. Just cast your view models to this interface and you can set / unset parent and child view models.

Nov 14, 2012 at 8:17 PM
For anybody that is interested. I've put two treeview approaches into an example app.

The first one uses dynamic viewmodels and binds treeviewitem specific properties to the viewmodel using an IValueConverter on the Datacontext. The same approach could propably be used for any user control with an ItemsSource. (just a note that treeview virtualization has not been tested) 

The second approach uses a viewmodel per model (non dynamic) and uses a wrapper class to keep the model graph and the viewmodelgraph in sync. (I haven't been able to test this approach with some of the more advanced features of catel.)

For my upcoming project i'll stick with the first option.

https://skydrive.live.com/redir?resid=8E285F74252F9EAF!443&authkey=!AI7joon5xZLKJ8Q
Martin
Jan 23, 2015 at 9:30 AM
Hello. I downloaded the example of Seaducer. I tried to reproduce it but viewmodel in TreeViewItemToDataContext is empty.
public class TreeViewItemToDataContext : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var uc = value as Catel.Windows.Controls.UserControl;
            if (uc != null)
            {
                var visualParent = uc.TemplatedParent as ContentPresenter;
                if (visualParent != null)
                {
                    var treeViewItem = visualParent.TemplatedParent as TreeViewItem;
                    if (treeViewItem != null)
                    {
                        var IsSelectedBinding = new Binding("IsSelected");
                        IsSelectedBinding.Mode = BindingMode.TwoWay;
                        IsSelectedBinding.Source = uc.ViewModel; // empty
                        treeViewItem.SetBinding(TreeViewItem.IsSelectedProperty, IsSelectedBinding);

                        var IsExpandedBinding = new Binding("IsExpanded");
                        IsExpandedBinding.Mode = BindingMode.TwoWay;
                        IsExpandedBinding.Source = uc.ViewModel; // empty
                        treeViewItem.SetBinding(TreeViewItem.IsExpandedProperty, IsExpandedBinding);
                    }
                }

                return uc.DataContext;
            }

            return Binding.DoNothing;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
How I Can set viewmodel for UserControl? I tried to use the constructor with a parameter for catel:UserControl (HotelUserControl, RoomUserControl, etc)but it did not help. I'm using Catel 4.
P.S. Maybe someone already thought up a better way to use Catel together with treeView?