Cannot convert viewmodel to model

Apr 3, 2012 at 12:16 PM

Hi Geert,

As you probably  know by now I have build a small planning project in WPF with the Telerik evaluation controls. It consist of a telerik gridview with nested controls on two levels. The model that I have bound to the grid is a list op Task that consist of a list of 5 Days. That Day has a list of persons.

This all works as expected.

Today we have bought the Telerik develoment controls (same version as the trail version) and now I am getting binding errors stating that the viewmodel cannot be converted to the model? All the properties that are in the viewmodel can also be found in the coresponding model.

I guess this is a Terik problem but the error's that I am getting seems Catel related.

See the error's below. I have noticed the target element name is empty (Name=''). I suspect this is part of the problem but I don't understand it.

In the Grid I am binding the dayview like this:

                        <telerik:GridViewDataColumn.CellTemplate>
                        <DataTemplate>
                            <Views:PlanBoardDayView DataContext="{Binding Days[0], Mode=TwoWay}"  />
                        </DataTemplate>
                    </telerik:GridViewDataColumn.CellTemplate>

 

Does this ring any bell?

Error:

System.Windows.Data Error: 23 : Cannot convert 'MySolution.Planning.PlanBoard.ViewModels.PlanBoardDayViewModel' from type 'PlanBoardDayViewModel' to type 'MySolution.Planning.PlanBoard.Models.PlanBoardDay' for 'nl-NL' culture with default conversions; consider using Converter property of Binding. NotSupportedException:'System.NotSupportedException: TypeConverter kan niet van MySolution.Planning.PlanBoard.ViewModels.PlanBoardDayViewModel worden geconverteerd.

bij System.ComponentModel.TypeConverter.GetConvertFromException(Object value)

bij System.ComponentModel.TypeConverter.ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, Object value)

bij MS.Internal.Data.DefaultValueConverter.ConvertHelper(Object o, Type destinationType, DependencyObject targetElement, CultureInfo culture, Boolean isForward)'

System.Windows.Data Error: 7 : ConvertBack cannot convert value 'MySolution.Planning.PlanBoard.ViewModels.PlanBoardDayViewModel' (type 'PlanBoardDayViewModel'). BindingExpression:Path=Days[0]; DataItem='PlanBoardTask' (HashCode=8505800); target element is 'PlanBoardDayView' (Name=''); target property is 'DataContext' (type 'Object') NotSupportedException:'System.NotSupportedException: TypeConverter kan niet van MySolution.Planning.PlanBoard.ViewModels.PlanBoardDayViewModel worden geconverteerd.

bij MS.Internal.Data.DefaultValueConverter.ConvertHelper(Object o, Type destinationType, DependencyObject targetElement, CultureInfo culture, Boolean isForward)

bij MS.Internal.Data.DefaultValueConverter.ConvertFrom(Object o, Type destinationType, DependencyObject targetElement, CultureInfo culture)

bij System.Windows.Data.BindingExpression.ConvertBackHelper(IValueConverter converter, Object value, Type sourceType, Object parameter, CultureInfo culture)'

Coordinator
Apr 3, 2012 at 12:23 PM

Ok, this might be a very complex answer, but I am just throwing it in.

Telerik is probably using a converter to convert the data context into a date. This converter throws an exception because something goes wrong. There are 2 reasons for this:

1) The binding is twoway. I have no idea whether you did this by purpose, but when you bind a datacontext, this happens:

  1. Parent sets datacontext of PlanBoardDayView to model
  2. PlanBoardDayView converts the model into a view model
  3. Because the binding is twoway, the Days[0] will be replaced by the view model <= I think this is unwanted behavior

2) In Catel 3.0, the actual datacontext of a user control is replaced by a view model. This has two downsides:

  1. See answer 1), you might have unwanted side effects
  2. You sometimes get binding errors that are not true (the binding *does* work)

In 3.1 we have found a solution for this by injecting a separate layer inside the user control. This way, the datacontext of the user control will stay as model, and internally it will receive the view model created based on the model.

So, there are actually 2 things you can do:

1) Disable the twoway binding (easiest fix)

2) Build the 3.1 source yourself and use that

Apr 3, 2012 at 1:23 PM

Hi Geert,

I have set the twoway binding with a purpose. The data of the dayview was shown correct but when I changed something the model wat not updated. At least I remember that the setter of the property was not called. So I thought maybe I have to set mode to twoway.

I have reverted my solution back to the original with the trail version of telerik and removed the twoway mode and to my surprise the setter where called and the model was updated.

Now I upgraded to to telerik dev controls again, removed the two way mode and all works fine :)

What I don't understand is why the model is updated if you change someting in the view? I always thought that was the idea behind tho way binding?

Anyway thank you for your great support, you saved my day!

 

Coordinator
Apr 3, 2012 at 1:28 PM

The fact that the model is not updated has nothing to do with this twoway binding. This binding is a one-way reference binding, so the user control twoway bindings are the reason the model is not updated.

The only way you need a two-way binding is if a model is created inside the child view model (thus, model injected in the vm is null, then the vm automatically creates one), but then you can only accomplish such a thing with 3.1.

Apr 4, 2012 at 7:37 AM

Ok, thanks for the answers.

Although it works fine I think I will download the 3.1 sources later on to build my own assemblies. This makes debugging easier and I get a better understanding of what's going on behind the screens.

Een dikke pluim voor jou!

Coordinator
Apr 4, 2012 at 7:45 AM

:)

It is possible to debug the official (and beta) releases of NuGet as well, see this documentation. But, still it is good to try to understand what is happening behind the screens, then you can use the full potential.

Apr 5, 2012 at 1:12 PM

Hi Geert,

Now I remeber why I added the mode = twoway option!

My model is an ObservableCollection of PlanBoardTasks that contains an ObservableCollection of 5 PlanBoardDays. it is named Tasks and is a vm property in the MainMindowViewModel. The days are named Days is a property in the PlanboardTaks model. I have no viewmodel for the task since I dont have a view for it and am directly binding to in instance of a day.

For the PlanBoardDay I have created a separater PlanBoardDayView and PlanBoardDayViewModel.

There I definded the model and other properties like this:

        /// <summary>
        /// Gets or sets the property value.
        /// </summary>
        [Model]
        public PlanBoardDay PlanBoardDay
        {
            get { return GetValue<PlanBoardDay>(PlanBoardDayProperty); }
            private set { SetValue(PlanBoardDayProperty, value); }
        }

        /// <summary>
        /// Register the PlanBoardDay property so it is known in the class.
        /// </summary>
        public static readonly PropertyData PlanBoardDayProperty = RegisterProperty("PlanBoardDay", typeof(PlanBoardDay));
        // TODO: Register view model properties with the vmprop or vmpropviewmodeltomodel codesnippets
        /// <summary>
        /// Gets or sets the property value.
        /// </summary>
        [ViewModelToModel("PlanBoardDay")]
        public DateTime Date
        {
            get { return GetValue<DateTime>(DateProperty); }
            set { SetValue(DateProperty, value); }
        }

        /// <summary>
        /// Register the Date property so it is known in the class.
        /// </summary>
        public static readonly PropertyData DateProperty = RegisterProperty("Date", typeof(DateTime));

In the main grid I have a telerikgrid that binds to the Tasks like this:

ItemsSource="{Binding Tasks}"

 allong with some other data I am binding 5 instances of the dayview. This is the column for the first day:

                <telerik:GridViewDataColumn CellStyle="{DynamicResource NoMargin}" DataMemberBinding="{Binding Empty}">
                        <telerik:GridViewDataColumn.Header>
                            <StackPanel>
                                <TextBlock HorizontalAlignment="Center">
                                <TextBlock.Text>
                                    <Binding RelativeSource="{RelativeSource AncestorType=telerik:GridViewDataControl}" Path="DataContext.Tasks[0].Days[0].WeekString" Mode="OneWay" />
                                </TextBlock.Text>
                                </TextBlock>
                                <TextBlock HorizontalAlignment="Center">
                                <TextBlock.Text>
                                    <Binding RelativeSource="{RelativeSource AncestorType=telerik:GridViewDataControl}" Path="DataContext.Tasks[0].Days[0].Date" StringFormat="{}{0:D}"/>
                                </TextBlock.Text>
                            </TextBlock>
                            </StackPanel>
                        </telerik:GridViewDataColumn.Header>
                        <telerik:GridViewDataColumn.CellTemplate>
                        <DataTemplate>
                            <Views:PlanBoardDayView DataContext="{Binding Days[0]}"/>
                        </DataTemplate>
                    </telerik:GridViewDataColumn.CellTemplate>
                </telerik:GridViewDataColumn>

in DataMemberBinding I can put any property but one has to be there. If I leave it away  not I get an error: Argument 'propertyName' cannot be null or whitespace. Parameternaam: propertyName. That is stange but with DataMemberBinding it works.

I have added two buttons "previous day" and "next day". If I hit next for example I add a new day with date last day +1 to the end of the days list in every task and I remove the first day in the days list of every task.

This was working but not reflected in the gui. Then I added the famous mode=twoway to the binding of the PlanBoardDayView like this:


<Views:PlanBoardDayView DataContext="{Binding Days[0], Mode=TwoWay}"/>

 After that when I hit the next button the for last 4 days where skipped back one position and the last day was showing the new day data.

Then I upgraded to the telerik dev controls and I got the the error in the first post so I removed the mode = twoway option. It cleared the error's but now my days are not refreshed anymore. In the header I am showing the date and this day gets refreshed though!

Any thoughts on this?

Coordinator
Apr 8, 2012 at 8:01 AM

I think I understand. Can you show the logic where you go to the next and previous dates? I would like to see the command implementation. It should be handled in the view model.

Apr 9, 2012 at 6:08 AM
Edited Apr 9, 2012 at 6:16 AM

Of course I can. I will show the code for the next button. The button itself is in the header of the last column:

                <telerik:GridViewDataColumn DataMemberBinding="{Binding Empty}">
                    <telerik:GridViewDataColumn.Header>
                            <Button x:Name="ButtonNext" Margin="0,0,5,0" Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type telerik:GridViewDataControl}}, Path=DataContext.NextDay}">
                            <Image Source="/Images/calendar_next_24.ico"></Image>
                        </Button>
                    </telerik:GridViewDataColumn.Header>
                </telerik:GridViewDataColumn>

DataMemberBinding is bound to Empty which is a property returning an empty string.

The command NextDay is registered in the constructor of the MainWindowViewModel like this:

NextDay = new Command(OnNextDayExecute);

It is implemented like this:

        /// Gets the NextDay command.
        /// </summary>
        public Command NextDay { get; private set; }

        /// <summary>
        /// Method to invoke when the NextDay command is executed.
        /// </summary>
        private void OnNextDayExecute()
        {
            Random rnd1 = new Random();
            foreach (PlanBoardTask taskrow in Tasks)
            {
                PlanBoardDay rowDayLast = new PlanBoardDay(taskrow.Days[4].Date.AddDays(1), rnd1.Next(10), rnd1.Next(10), rnd1.Next(100), rnd1.Next(100), taskrow);

                PlanBoardResource res1rodDay0 = new PlanBoardResource("No", "Michel", new TimeSpan(9, 0, 0), new TimeSpan(16, 0, 0), "Def");
                rowDayLast.PlannedResources.Add(res1rodDay0);
                PlanBoardResource res2rodDay0 = new PlanBoardResource("No", "Mike", new TimeSpan(8, 0, 0), new TimeSpan(16, 0, 0), "Ann");
                rowDayLast.PlannedResources.Add(res2rodDay0);

                taskrow.Days.Add(rowDayLast);
                taskrow.Days.RemoveAt(0);
            }
        }

I am using some mock data for now. The real data will come from a wcf service in a later stage. The most important parameter in the constructor is the first which is the date. Also the day can have zero or more resources (persons planned for this day). When this code runs the model is updated. The bound dates in the grid header gets updated but the PlanBoardDayView does not.

 

Coordinator
Apr 10, 2012 at 6:17 PM

Make sure to try the latest source of Catel (represents 3.1), I think it has some improvements that may help to address this issue.

Apr 11, 2012 at 12:47 PM

I am using the latest 3.1 version and it solved my issues!

Now I one more question. In the planBoardDay I have a property QuantityPlanned which is bound in the gui. This should be a read only property that counts the number of PlannedResource which is a observableCollection of PlanBoardResources.

When I add or remove a PlanBoardResource from PlannedResources it should be refreshed in the gui.

I solved it in the PlanBoardDay like this. 

        /// <summary>
        /// Gets or sets the property value.
        /// </summary>
        public float QuantityPlanned
        {
            //get { return GetValue<float>(QuantityPlannedProperty); }
            get
            {
                return PlannedResources.Count;
            }
            private set { SetValue(QuantityPlannedProperty, value); }
        }

        /// <summary>
        /// Register the QuantityPlanned property so it is known in the class.
        /// </summary>
        public static readonly PropertyData QuantityPlannedProperty = RegisterProperty("QuantityPlanned", typeof(float), 0);

 

In the PlanBoardDayViewModel I set the number when a resource is added or removed like this:

        #region Commands
        // TODO: Register commands with the vmcommand or vmcommandwithcanexecute codesnippets

        /// <summary>
        /// Gets the AddResource command.
        /// </summary>
        public Command AddResource { get; private set; }

        /// <summary>
        /// Method to invoke when the AddResource command is executed.
        /// </summary>
        private void OnAddResourceExecute()
        {
            PlannedResources.Add(new PlanBoardResource(string.Format("ResourceNo {0}", _resourceIndex++), string.Format("ResourceName {0}", _resourceIndex++), TimeSpan.Zero, new TimeSpan(23, 59, 0), "Status"));
            QuantityPlanned++;
        }

        /// <summary>
        /// Gets the RemoveResource command.
        /// </summary>
        public Command RemoveResource { get; private set; }

        /// <summary>
        /// Method to invoke when the RemoveResource command is executed.
        /// </summary>
        private void OnRemoveResourceExecute()
        {
            // TODO: Handle command logic here
            PlannedResources.Remove(SelectedResource);
            QuantityPlanned--;
        }

        #endregion

This works but I was wondering if there is a better way.

If I add or remove a resource I need to changed the QuantityPlanned property or else it is not refreshed. Is there a way to automaticly "update" the QuantityPlanned property when the PlannedResources property changes?

BTW you can add or remove resources with the context (right mouse click) in the PlanBoardDayView.


 

Coordinator
Apr 12, 2012 at 6:51 AM

If it is a calculated property, you shouldn't make it a Catel property (because you don't even need a setter):

public int QuantityPlanned { get { return PlannedResources.Count; } }

You then have 2 options:

1) Call RaisePropertyChanged(() => QuantityPlanned) in OnAddResourceExecute (easiest way)

2) Subscribe to CollectionChanged and call the RaisePropertyChanged there (but care for memory leaks and event subscriptions that you do not unsubscribe, you might be interested in the WeakEventListener of Catel to solve this

Apr 13, 2012 at 9:03 AM
Edited Apr 13, 2012 at 9:05 AM

Option 1 works like a charm. I had more or less the same idea but I did not know it was possible to refresh "normal" properties with the raisepropertychage call. Once again a big thank you.

I have more calculated properties that depend on other properties. Is there a way I can say if property X changes raisepropertychanged for propery Y?

Coordinator
Apr 13, 2012 at 9:14 AM

Well, in catel you can register automatic property changes. For example, when you register a property like this:

public DateTime Date
{
	get { return GetValue<DateTime>(DateProperty); }
	set { SetValue(DateProperty, value); }
}

public static readonly PropertyData DateProperty = RegisterProperty("Date", typeof(DateTime), null, (s, e) => ((MyOwnerClass)s).OnDateChanged());

private void OnDateChanged()
{
    RaisePropertyChanged(() => MyProperty);
}

This can also be written like this:

public DateTime Date
{
	get { return GetValue<DateTime>(DateProperty); }
	set { SetValue(DateProperty, value); }
}

public static readonly PropertyData DateProperty = RegisterProperty("Date", typeof(DateTime), null, (s, e) => ((MyOwnerClass)s).RaisePropertyChanged("MyProperty"));

Apr 13, 2012 at 9:31 AM
Edited Apr 14, 2012 at 11:59 AM

Should I do this in the model, viewmodel or in both?

Coordinator
Apr 13, 2012 at 9:38 AM
Edited Apr 13, 2012 at 9:38 AM

Well, I assume that MyProperty is a calculated property. If such a property is mapped, you only have to do this in the source (thus model). Because this will happen:

1) Property on VM changes

2) VM picks this up, maps to the model

3) Model property changes

4) Model invokes the RaisePropertyChanged for calculated property

5) VM picks this up, maps the to the view model

6) There you got your change notification :)

Apr 14, 2012 at 12:23 PM
Edited Apr 14, 2012 at 12:27 PM

I have made all my calculated properties in the vm only and raise the propertychanged there. This works fine but  is it a valid contruction? I guess they must be in the vm since the gui is binding to the vm.

If I wanted the calculated properties in my model too I could make them there the same way as in the vm and only raise the property changed in the model right? Or should I make my calculated peroprty in the model only and only map it to vm? How do i do this?

The calculated properties in my PlanBoardDayViewModel are now all updated. Now I have properties in my PlanBoardResourceViewModel that is nested in the PlanBoardDayViewModel. When I add code to update properties in its parent PlanBoardDayViewModel I get an error like this: Kan een object van het type MySolution.Planning.PlanBoard.ViewModels.PlanBoardResourceViewModel niet converteren naar het type MySolution.Planning.PlanBoard.ViewModels.PlanBoardDayViewModel.

In the PlanBoardResourceView I changed

public static readonly PropertyData StartTimeProperty = RegisterProperty("StartTime", typeof(TimeSpan), TimeSpan.Zero);
        

to:

public static readonly PropertyData StartTimeProperty = RegisterProperty("StartTime", typeof(TimeSpan), TimeSpan.Zero, (s, e) => ((PlanBoardDayViewModel)s).OnTimeChanged());

 

In the PlanBoardDayViewModel I want to update the calculated properties in thr gui like this:

        public void OnTimeChanged()
        {
            RaisePropertyChanged(() => QuantityNeeded);
            RaisePropertyChanged(() => HoursNeeded);
            RaisePropertyChanged(() => HoursRequired);
        }

This construction raises the aboce exception.

So In short my questions:

1. Where should i make my calculated properties and how do I map them to vm?

2. Where should I raise the propertychanged?

3. How can I raise propertychanged for properties in other level model or viewmodel?

4. Can you give a small example of model and view model with a property and  a calculated property?

 

Apr 16, 2012 at 8:34 AM

I have done more research. When I create the calculated properties in the vm only and raise the propertychanged there all works fine.

But I like to have the calculated properties in my model. So I moved them to the model. In the PlanBoardDayviewModel I maped the properties like this:

public int QuantityPlanned { get { return PlanBoardDay.QuantityPlanned; } }

I also moved the raisepropertychanged to the model. When I now change the property in the gui the proprties in the model are changed but in the vm not.

What am I mising?

And my other question is, can I call propertychanged functions in other model or view model? If I do this I get an conversion error.

 

 

Coordinator
Apr 17, 2012 at 11:02 AM

1) Do you map the read-only calculated properties in the view model as well?

2) You should not call a property change for vm x in vm y. It's the responsibility of each vm to raise their own property changed events.