MVVM - How do I add a property to a child object?

MVVM - How do I add a property to a child object?

Old forum URL: forums.lhotka.net/forums/t/8493.aspx


Kevin Fairclough posted on Monday, February 08, 2010

Hi All you MVVM'ers

Currently my BO is structured as follows: Item (Root), ItemsLinked (ROList) contains (ROItem) contains ItemsLinked (ROList)  which is an n-level hierarchy of items lazy-loaded.

I'm using MVVM to display the hierarchy in WPF using a HierarchicalDataTemplate and all is working ok and it is displaying correctly.

The problem I have is that using HierarchicalDataTemplate or any DataTemplate for that matter requires the Type to be set, in my case ROItem is the type that is in the hierarchy which is also the BO type.  This was ok for just displaying but now I need to decorate the type with another property EditMode so I can toggle edit state in the hierarchy, and show edit controls etc.  I shouldn't add EditMode to my BO (which would be a lot easier!)

Using the CSLA MVVM classes, how do I now do this?  My data template should point to ROItemVM which will have the EditMode property, but now what happens to my ROList it needs to contain a list of ROItemVM not ROItem.

I should end up with ItemVM contains ROListVM contains ROItemVM containg ROListVM and so on...

Has anyone done something similar as I'm stuck

TIA

Kevin

 

 

 

RockfordLhotka replied on Monday, February 08, 2010

The key here is to understand that the VM is tied to the V, not the M.

Creating a VM for every M type is wrong - at least if you have a rich model.

Many/most MVVM examples assume what's called an anemic Model - a Model composed of dumb data container objects.

But CSLA objects are not anemic. Your business objects fully support data binding, encapsulate behavior (business, validation, authorization rules) and so forth.

Therefore it is just plain silly (and really hard) to wrap every business object type with a VM.

Instead, you should think about the VM as managing sections or regions of the screen. In fact, you should only have a VM if you need to add commands to the model for a region of the screen.

Pretty much every screen has the need for a top-level VM to provide top-level commands for the overall form. Only sometimes do you need to provide commands in sub-regions of the form, and in that case you'd need other VM objects - very likely chained off the Model of the top-level VM.

It is somewhat rare to need a VM for a list. Usually you need a VM for each item in the list, because you have some navigation or other command operations in the UI in each row of the grid - and those commands need to bind to a VM. That's easily done using a DataTemplate - just put the VM resource into the DataTemplate and away you go.

If you do need a VM for a list, that's fine too - look at the MVVMexample sample apps - I do this in the sample by chaining the list's VM off the Model from the top-level VM.

Kevin Fairclough replied on Monday, February 08, 2010

Thanks for the reply.

So.. For my requirement I need a view model for each item in the hierarchy, i.e. a view model for each child object in the list.

I have this HierarchicalDataTemplate but it isn't  bound correctly, I also noticed only 1 instance of the Viewmodel was being created not one for each item in the list.

<HierarchicalDataTemplate x:Key="ChildLinks"  ItemsSource="{Binding ChildLinks}" >       
        <HierarchicalDataTemplate.Resources>           
            <ViewModels:ItemInHierarchyViewModel x:Key="ViewModel" Model="{Binding /}"  />           
        </HierarchicalDataTemplate.Resources>

        <Border DataContext="{Binding Source={StaticResource ViewModel}}" ...Content of template.../>

</HierarchicalDataTemplate>

My treeview is being populated with items  but the display is not bound correctly to the ViewModel

Are there any examples out there for this.

TIA

Kevin

 

 

Kevin Fairclough replied on Thursday, February 11, 2010

Follow Up.

 

Using the ViewModel<T> for child objects works fine when behaviour only is involved.

The ViewModel as a static resource can be used to receive calls using csla:InvokeMethod as expected.

 

State on the other hand cannot be achieved from what I can see.

If you want to have in-place editing where the UI switches from a read only view to an edit view then you need to add the state property to the child object.

What I wanted was to wrap every child object so the EditMode state variable was kept out of my BO.

I cannot do this with CSLA ,from what I can gather.

 

Kevin

 

 

 

RockfordLhotka replied on Thursday, February 11, 2010

I'm not sure I follow?

CSLA isn't a UI framework, so whether you can or can't do something at the UI level is really outside the scope of CSLA. The View and ViewModel components are part of the UI layer.

It is true that I've done with XAML what I've done with other UI technologies, which is to include the barest possible set of UI helper functionality necessary to make app development possible. But I surely don't plan to extend CSLA into being a broader UI framework for XAML (or any other UI technology).

If you are unable to maintain state in a ViewModel<T> can you describe how you'd maintain state in another viewmodel implementation? And if you can do so, then you should almost certainly utilize that other approach.

Typically however, a UI region (whatever that means to your UI) can bind to a viewmodel that wraps a model. And by binding to the viewmodel, UI elements in that region can interact with methods and properties on the viewmodel - and by extension the Model property of the viewmodel.

Which is why I'm not sure I follow. If you are able to bind to methods on the viewmodel, why can't you bind to properties on the viewmodel?

Kevin Fairclough replied on Friday, February 12, 2010

Hi Rocky

Thanks for the reply.

The only way I can maintain UI state is by adding the state boolean to the CSLA child BO, which works as its bindable, but means I've got UI stuff inside my BO.

The thing is ViewModel<T> can wrap a root object with UI properties no bother, when it comes to using the same ViewModel<T> for child objects however the problem appears.  This is because the collection holding the child is a collection of ChildBO not ChildBOViewModel.

Other MVVM approaches use the anemic model and therefore the whole UI is bound to VMs containing other VM's and so on (for data as well as behaviour ) so they can easily decorate models with new UI properties.

The UI needs to bind to ViewModel's for data and behaviour, so the object graph of the BO should have an equivalent graph for the ViewModel;  Root > ChildCollection > Child should map to the UI as RootVM > ChildVMCollection - ChildVM

This is my first real screen trying to use MVVM & Csla.  It is a root object with a treeview for children (n levels deep),  its the treeview items that toggle edit state.

Thanks for your time

Kevin

 

ajj3085 replied on Friday, February 12, 2010

This kind of thing came up on WinForms too.  Usually the advice was to create a wrapper BO in your UI library which tracked the extra state, since the state wasn't really part of the BO.  Then the child list is bound using the wrapper list / wrapper BO.

RockfordLhotka replied on Friday, February 12, 2010

Andy is correct.

Here's the thing. I created the ViewModel<T> concept to simplify the common case of binding objects to a form. It is usually a good answer and makes life much easier (reducing coding/testing effort, etc).

But if it doesn't help your UI scenario then don't use it in that scenario. It isn't like you can't build a more complex viewmodel class if needed to support some specific UI scenario. The techniques and complexity will be pretty much the same as when working with an anemic model, so it isn't like you lose anything over what non-CSLA people have to put up with every day Smile

On the other hand, I wouldn't use those anemic techniques in every form, because they are a lot more work. Use them when necessary, and stick with the simpler ViewModel<T> approach for most forms.

Kevin Fairclough replied on Friday, February 12, 2010

Fair enough,  I guess I thought it would naturally work that way.

From my brief time with MVVM and CSLA  I cannot see why this approach (wrapping the CSLA graph) is not common place.  WPF is all about the binding and if your binding to a ViewModel in the first place for the root then you lose control over how the view affects the root models graph, you are relying on automatic behaviour of controls to change state.

My original "fix" for the edit flag (in the child BO) also doesn't work because it fires an edit on the actually BO (Obviously Tongue Tied ).

All this pain for a togglebutton controlling row edit mode Sad

 

Thanks

Kevin

 

 

 

sergeyb replied on Friday, February 12, 2010

I think you can probably just add CurrentSelectedItem And CurrentSelectedItemEditMode properties to your VM and binding your visuals to VM. CurrentSelectedItemEditMode.  Then you should be able to add a SelectItemCommand to your VM.  You will need to extend the Tree view control and add a ItemSelectedCommand property to it that will be invoked from within SelectionCHanged event.  This way when SelectionCHanged event fires on your tree view, your

SelectItemCommand in VM is invoked, where you can have a dictionary< Item, EditMode> that you will maintain and use from CurrentSelectedItemEditMode property.  In any case, you can always inherit base view model in CSLA and extend it to fit your specific needs on certain forms.

 

I hope this helps.

Kevin Fairclough replied on Tuesday, February 16, 2010

Yes this sounds like an alternative solution to having to wrap the object graph, will look at this, although it still feels like a lot of work.

I made my boolean EditMode a non managed property in my ChildBO, and manually fired OnPropertyChanged for it, which is a hack @ the moment for my prototype, but works.

 

Thanks for your time

Kevin

RockfordLhotka replied on Tuesday, February 16, 2010

I've been working a bit with the VS10 designer over the past week, trying to figure out a couple things, including a solution to this question.

One thing to consider is the separation of concerns expressly implied by the MVVM pattern.

You can consider that you have the view technologies, which includes all XAML types and all view types (like your forms and user controls). The view can be aware of viewmodel and model types.

Then you have the viewmodel technologies, which includes all viewmodel types (your viewmodels). The viewmodel can be aware of model types, but can not be aware of view types.

Then you have the model technologies, which includes the model types (your business classes), ADO.NET, WCF, WF and whatever else you are using to construct your model and related plumbing. The model is not aware of view or viewmodel types.

So far, so good.

In your case you want the view to bind to a root viewmodel, which is easy. And then you want a list/datagrid/??? to bind to a list of viewmodel objects, each of which wraps a specific child, because each child needs verbs or extra properties.

The thing is, this is really a "child viewmodel", which means it must come from the parent viewmodel. It can't come out of thin air.

What I've been trying, and I don't know if it solves your specific issue, is exactly that. Have the parent viewmodel create the child viewmodel list. In my case this list doesn't need to be a viewmodel itself, it is just a list of child viewmodel objects.

So in my root viewmodel I have verbs related to the overall form. And a property to expose the child viewmodel list.

public List<ChildViewModel> Children
{
  get
  {
    return new List<ChildViewModel>(
      Model.Children.Select(r => new ChildViewModel(r)));
  }
}

With this property in the root viewmodel class, I can now bind a UI element (listbox/datagrid/etc) to this Children property. The UI is thereby bound to a list of child viewmodel objects that extend individual child business objects.

Kevin Fairclough replied on Tuesday, February 16, 2010

On the face of it that is exactly what I was looking for, not being a Linq user currently I hadn't even thought about it that way.  I knew that's what I wanted but achieving it was put on hold Smile

The question is does Linq cascade changes through to the underlying collection, if so, we're in business.

You're a Star Rocky!

RockfordLhotka replied on Tuesday, February 16, 2010

What do you mean by "cascade changes"?

If you are looking for add/delete operations on the list of viewmodel objects to affect the business objects you are out of luck unless you write that yourself. Remember that the UI is bound to a list of viewmodel objects, not to a list of business objects.

But if you are looking for changes to the child viewmodel Model property to affect the business object, you'll get that - because the Model property of each child viewmodel object is the actual business object. As with other ViewModel<T> scenarios, the viewmodel isn't really a "wrapper" as much as an "extension" of the underlying business object.

Kevin Fairclough replied on Tuesday, February 16, 2010

Ah! ok, we're still not quite there then (for what I want) 

The View (for an editable screen) will be using the ChildListViewModel to add/delete/modify the contents of the internal CSLA childcollection.  What you are suggesting won't get me that.  I will still need a full wrapper.

For me it will be complicated further by the fact it's a hierarchical object.

Thanks

Kevin

 

 

 

RockfordLhotka replied on Tuesday, February 16, 2010

You should be able to use the same technique I'm talking about - you just need to return a viewmodel that is a collection, rather than a simple List<whatever>.

The only hard part about doing that is that you'll need to create this list-oriented viewmodel object, and echo any add/remove actions down to the underlying business list. You'll almost certainly want to make your list derive from ObservableCollection<T>, which does have notification mechanisms (CollectionChanged event for example) so you can easily determine that an item was added/removed from the viewmodel and so echo that action to the underlying business list.

This only gets truly complex if you allow direct manipulation of the business list while it is bound, because then you'd need to do bi-directional echoing of events and so forth - and that does get pretty tricky. But if this is always one-way - where only the UI is manipulating the list or elements in the list, then it isn't too bad.

simon replied on Monday, August 29, 2011

I encounter on the same problem.  according to Rocky's  solution I set up a childviewmodel to wrap the added property. code as below:

Root ViewModel:

 public class MaterialCategorysViewModel:ViewModel<MaterialCategorys>

    {      

        public ObservableCollection<MaterialCategoryViewModel> RootMaterialCategorys

        {

            get

            {              

                var result = new ObservableCollection<MaterialCategoryViewModel>();

                if (Model != null)

                    foreach (var item in Model)

                        result.Add(new MaterialCategoryViewModel(item));

 

                return result;

            }

        }

 

        #region Constructor

        [ImportingConstructor]

        public MaterialCategorysViewModel(IRegionManager regionManager, IEventAggregator eventAggregator)

        {

 

            InitData();

 

        }

        #endregion

 

        private void InitData()

        {

            BeginRefresh("GetMaterialCategorys");

        }

 

        private  void AddRootNode()

        {

           var SelectedItem= Model.AddRootNode();

 

        }

 

        private  void AddChildNode(MaterialCategory item)

        {

          var newItem= Model.AddChildNode(item);

 

        }

 

       protected override void OnModelChanged(MaterialCategorys oldValue, MaterialCategorys  newValue)

        {

            base.OnModelChanged(oldValue, newValue);

            if (oldValue != null)

            {

                newValue.CollectionChanged -= new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Model_CollectionChanged);

            }

 

            if (newValue != null)

            {

                newValue.CollectionChanged +=new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Model_CollectionChanged);

 

            }

            OnPropertyChanged("RootMaterialCategorys");

        }

 

        private void Model_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)

        {

            OnPropertyChanged("RootMaterialCategorys");

        }

     }

ChildViewModel:

 public class MaterialCategoryViewModel:DependencyObject,INotifyPropertyChanged

    {

        bool _isExpanded;

        bool _isSelected;

 

        public static readonly DependencyProperty ModelProperty =

        DependencyProperty.Register("Model", typeof(MaterialCategory), typeof(MaterialCategoryViewModel), null);

 

        public MaterialCategory Model

        {

            get { return (MaterialCategory)GetValue(ModelProperty); }

            set { SetValue(ModelProperty, value); }

        }

 

        public MaterialCategoryViewModel(MaterialCategory model) 

        {

            Model = model;

 

            if (model.ChildMaterialCategorys != null) 

                model.ChildMaterialCategorys.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Model_CollectionChanged);

                model.PropertyChanged +=new PropertyChangedEventHandler(model_PropertyChanged);

 

           // model.ChildMaterialCategorys.

 

        }

 

        private void model_PropertyChanged(object sender, PropertyChangedEventArgs e)

        {

            if (e.PropertyName == "ChildMaterialCategorys")

            {

                if (Model.ChildMaterialCategorys != null)

                {

 

                        OnPropertyChanged("Children");

 

                }

            }

 

 

        }

 

 

        private ObservableCollection<MaterialCategoryViewModel> _children;

        public ObservableCollection<MaterialCategoryViewModel> Children

        {

            get

            {

                var result = new ObservableCollection<MaterialCategoryViewModel>();

                if(Model.ChildMaterialCategorys !=null)

                   foreach (MaterialCategory c in Model.ChildMaterialCategorys)

                           result.Add(new MaterialCategoryViewModel(c));

 

                return result;

 

            }

        }

 

 

        private void Model_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)

        {

            OnPropertyChanged("Children");

        }

 

        #region IsExpanded

 

        /// <summary>

        /// Gets/sets whether the TreeViewItem 

        /// associated with this object is expanded.

        /// </summary>

 

 

        #endregion // IsExpanded

 

        #region IsSelected

 

        public bool IsSelected

        {

            get { return _isSelected; }

            set

            {

                if (value != _isSelected)

                {

                    _isSelected = value;

                    this.OnPropertyChanged("IsSelected");

                }

            }

        }

 

        #endregion // IsSelected

 

        #region INotifyPropertyChanged Members

 

        public event PropertyChangedEventHandler PropertyChanged;

 

        protected virtual void OnPropertyChanged(string propertyName)

        {

            if (this.PropertyChanged != null)

                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

        }

        #endregion // INotifyPropertyChanged Members

    }

View:

<TreeView Name="CategoryTreeView" ItemsSource="{Binding Path=RootMaterialCategorys}"  

                 ItemTemplate ="{StaticResource NavigationTemplate }" >

                    <TreeView.ItemContainerStyle>

                        <!-- This Style binds a TreeViewItem to a PersonViewModel.  -->

                        <Style TargetType="{x:Type TreeViewItem}">

                            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />

                            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />

                            <Setter Property="FontWeight" Value="Normal" />

                            <Style.Triggers>

                                <Trigger Property="IsSelected" Value="True">

                                    <Setter Property="FontWeight" Value="Bold" />

                                </Trigger>

                            </Style.Triggers>

                        </Style>

                    </TreeView.ItemContainerStyle>

 

                </TreeView>

 <HierarchicalDataTemplate x:Key="NavigationTemplate" 

                                  ItemsSource="{Binding Path=Children}" >

            <StackPanel Orientation="Horizontal" Margin="0">

                <Border Width="100" BorderBrush="LightBlue" BorderThickness="1">

                    <TextBlock  Text="{Binding Model.Id}"/>

                </Border>

                <Border Width="100" BorderBrush="LightBlue" BorderThickness="1">

                    <TextBlock  Text="{Binding Model. Name}"/>

                </Border>

                <Border Width="100" BorderBrush="LightBlue" BorderThickness="1">

                    <TextBlock Text="{Binding Model.Description}"/>

                </Border>

            </StackPanel>

        </HierarchicalDataTemplate>

Model:

  public static readonly PropertyInfo<MaterialCategorys> ChildMaterialCategorysProperty = RegisterProperty<MaterialCategorys>(c => c.ChildMaterialCategorys, RelationshipTypes.Child | RelationshipTypes.LazyLoad);

        public MaterialCategorys ChildMaterialCategorys

        {

            get

            {

                if (!FieldManager.FieldExists(ChildMaterialCategorysProperty))

                {

                    DataPortal.BeginFetch<MaterialCategorysCreator>(Id, (o, e) =>

                    {

                        if (e.Error != null)

                            throw e.Error;

                        else

                            ChildMaterialCategorys = e.Object.Result;

 

                    });

                    return null;

                }

                return GetProperty(ChildMaterialCategorysProperty);

            }

            set

            {

                LoadProperty(ChildMaterialCategorysProperty, value);

                OnPropertyChanged(ChildMaterialCategorysProperty);

            }

        }

 

Problem:

1、 I  found model ChildMaterialCategorys  access the same Id  twice at the top  and second level.  

2、when add top-level item into the Materialcategorys(  BusinessListBase<MaterialCategorys, MaterialCategory>), the view will rebinding all child.

is there a better approach to treat treeview databinding?

 

 

 

 

Copyright (c) Marimer LLC