locked
Weird binding error while trying to edit typed DataTemplate RRS feed

  • Question

  • Hi,

    I have a ViewModel (TestViewModel) which has a read only "Name" property. In this ViewModel I have property named Model(TestModel) which also has a "Name" property which is NOT read only. Then I defined a DataTemplate for TestViewModel to show Name and Description fields of it's Model property.

    Now when I open MainWindow.xaml from "Expression Blend" and from the resources tab try to edit this DataTemplate I get the following error:

    "A TwoWay or OneWayToSource binding cannot work on the read-only property 'Name' of type 'DataTemplateError.TestViewModel'."

    The DataTemplate works as expected when the project runs; so the bindings are correct. But when trying to edit/design DataTemplate through Blend it's not working. Although I set the DataContext of StackPanel to Model it's giving the error as if the TextBox for "Name" property is bound to read only "Name" property of TestViewModel:

    I've supplied the necessary code files below and I'm using Expression Blend 4 SP1.

    Thanks in advance

    ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    TestModel class is below:

    using System;
    using System.ComponentModel;

    namespace DataTemplateError {

      public class TestModel : INotifyPropertyChanged {

        private string _Name = "Runtime Model Name";
        private string _Description = "Runtime Model Description";

        public string Name {
          get { return this._Name; }
          set {
            _Name = value;
            this.NotifyPropertyChanged("Name");
          }
        }

        public string Description {
          get { return this._Description; }
          set {
            _Description = value;
            this.NotifyPropertyChanged("Description");
          }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String info) {
          if (PropertyChanged != null) {
            PropertyChanged(thisnew PropertyChangedEventArgs(info));
          }
        }

      }
    }

    TestViewModel class is below:

    using System;
    using System.ComponentModel;

    namespace DataTemplateError {

      public class TestViewModel : INotifyPropertyChanged {

        private string _Name = "Runtime ViewModel Name";

        public string Name {
          get { return this._Name; }
          private set {
            _Name = value;
            this.NotifyPropertyChanged("Name");
          }
        }

        private TestModel _Model = new TestModel();

        public TestModel Model {
          get { return this._Model; }
          set {
            _Model = value;
            this.NotifyPropertyChanged("Model");
          }
        }

        public void UpdateNameMethod() {
          if (!this.Name.EndsWith("Updated Value"StringComparison.Ordinal)) {
            Name = Name + " - Updated Value";
          }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String info) {
          if (PropertyChanged != null) {
            PropertyChanged(thisnew PropertyChangedEventArgs(info));
          }
        }

      }
    }

    MainWindow.xaml is below:

    <Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:sys="clr-namespace:System;assembly=mscorlib"
            xmlns:local="clr-namespace:DataTemplateError"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
            mc:Ignorable="d" x:Class="DataTemplateError.MainWindow"
            Title="MainWindow" Height="350" Width="525">
      <Window.Resources>
        <local:TestViewModel x:Key="vmTest" />

        <DataTemplate DataType="{x:Type local:TestViewModel}">
          <StackPanel Orientation="Horizontal" DataContext="{Binding Model}">
            <TextBox Text="{Binding Name}" />
            <TextBox Text="{Binding Description}" />       </StackPanel>
        </DataTemplate>

      </Window.Resources>
      <Grid DataContext="{Binding Source={StaticResource vmTest}}">
        <Grid.RowDefinitions>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto"/>
          <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Label Content="ViewModel Name:" Grid.Row="0" Grid.Column="0" />
        <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Name, Mode=OneWay}" />
        <Label Content="Model Name and Descr:" Grid.Row="1" Grid.Column="0" />
        <ContentControl Grid.Row="1" Grid.Column="1" Content="{Binding}"/>
      </Grid>
    </Window>
    Thursday, October 14, 2010 8:08 PM

All replies

  • It looks like it is because it is actually targeting the Name property on your TestViewModel class, which has a private set method:

     

     public string Name {
       get { return this._Name; }
       private set {
        _Name = value;
        this.NotifyPropertyChanged("Name");
       }
      }

    Thursday, October 14, 2010 9:42 PM
    Moderator
  • Sorry but I didn't understand your explanation. I know the error says that but the TextBlock's DataContext that is bound to "Name" property is "Model", NOT TestViewModel. I set the DataContext of StackPanel to "Model". And also when I run the app it binds to the Model's Name property NOT TestViewModel's.

    <DataTemplate DataType="{x:Type local:TestViewModel}">
     <StackPanel Orientation="Horizontal" DataContext="{Binding Model}">
      <TextBox Text="{Binding Name}" />
      <TextBox Text="{Binding Description}" />
       
     </StackPanel>
    </DataTemplate>

    So I didn't understand why you said "it is actually targeting the Name property on your TestViewModel class".

    Can you explain it a little bit more? What am I missing?

    Thanks in advance

    Thursday, October 14, 2010 10:16 PM
  • Can someone please give a detailed explanation why I get this error? 
    Friday, October 15, 2010 8:59 AM
  • You are correct that it should be binding to the property on the model class, but it appears to not be. It does seem to work at runtime though.  We'll investigate this as a possible bug.

    In terms of fixing the problem you are experiencing, the way this is set up is a bit outside of the intent of the MVVM pattern. Ideally the V (view) is bound to the VM (viewmodel) which exposes all needed properties. The model shouldn't be directly coupled to the view. If it were me, I would make the Model property private, and have the properties on your VM wrap the values in the model. The you don't need the DataContext of the stackpanel set to Model, and you better isolate/decouple your V and M layers.

    Here is your example turned a bit more into the MVVM pattern. I also added an example of a command to the VM, to show how the UI can interact with the model, but isolated from the model itself.

    public class TestModel
    	{
    
    		public TestModel()
    		{
    			this.Name = "Runtime Model Name";
    			this.Description = "Runtime Model Description";
    
    		}
    		public string Name { get; set; }
    		public string Description { get; set; }
    	}
    
    
    	public class TestViewModel : INotifyPropertyChanged
    	{
    		public TestViewModel()
    		{
    			this.Model = new TestModel();
    			this.ChangeNameCommand = new ChangeNameCommand(this);
    		}
    
    		private TestModel Model { get; set; }
    
    		private string _Name = "Runtime ViewModel Name";
    
    		public string Name
    		{
    			get { return this._Name; }
    			private set
    			{
    				_Name = value;
    				this.NotifyPropertyChanged("Name");
    			}
    		}
    
    		public string ModelName
    		{
    			get { return this.Model.Name; }
    			set
    			{
    				if (this.Model.Name != value)
    				{
    					this.Model.Name = value;
    					this.NotifyPropertyChanged("ModelName");
    				}
    			}
    		}
    
    		public string ModelDescription
    		{
    			get { return this.Model.Description; }
    			set
    			{
    				if (this.Model.Description != value)
    				{
    					this.Model.Description = value;
    					this.NotifyPropertyChanged("ModelDescription");
    				}
    			}
    		}
    
    
    		public void UpdateNameMethod()
    		{
    			if (!this.Name.EndsWith("Updated Value", StringComparison.Ordinal))
    			{
    				Name = Name + " - Updated Value";
    			}
    		}
    
    
    		public ICommand ChangeNameCommand { get; set; }
    
    
    		public event PropertyChangedEventHandler PropertyChanged;
    
    		private void NotifyPropertyChanged(String info)
    		{
    			if (PropertyChanged != null)
    			{
    				PropertyChanged(this, new PropertyChangedEventArgs(info));
    			}
    		}
    	}
    
    	public class ChangeNameCommand : ICommand
    	{
    		public ChangeNameCommand(TestViewModel vm)
    		{
    			this.ViewModel = vm;
    		}
    
    		public TestViewModel ViewModel { get; set; }
    		#region ICommand Members
    
    		public bool CanExecute(object parameter)
    		{
    			return true;
    		}
    
    		public event EventHandler CanExecuteChanged;
    
    		public void Execute(object parameter)
    		{
    			if (parameter is string)
    			{
    				this.ViewModel.ModelName = (string)parameter;
    			}
    		}
    
    		#endregion
    	}
    
    <Window.Resources>
    		<local:TestViewModel x:Key="vmTest" />
    
    		<DataTemplate DataType="{x:Type local:TestViewModel}">
    			<StackPanel Orientation="Horizontal">
    				<TextBox Text="{Binding ModelName}" />
    				<TextBox Text="{Binding ModelDescription}" />
    				<Button Content="Change Model name" Command="{Binding ChangeNameCommand}" CommandParameter="New name here" />
    			</StackPanel>
    		</DataTemplate>
    
    	</Window.Resources>
    	<Grid DataContext="{Binding Source={StaticResource vmTest}}">
    		<Grid.RowDefinitions>
    			<RowDefinition Height="Auto"/>
    			<RowDefinition Height="Auto"/>
    			<RowDefinition />
    		</Grid.RowDefinitions>
    		<Grid.ColumnDefinitions>
    			<ColumnDefinition Width="Auto"/>
    			<ColumnDefinition />
    		</Grid.ColumnDefinitions>
    		<Label Content="ViewModel Name:" Grid.Row="0" Grid.Column="0" />
    		<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Name, Mode=OneWay}" />
    		<Label Content="Model Name and Descr:" Grid.Row="1" Grid.Column="0" />
    		<ContentControl Grid.Row="1" Grid.Column="1" Content="{Binding}"/>
    	</Grid>
    

    Friday, October 15, 2010 1:40 PM
    Moderator
  • This does look like a but in Blend. Could you workaround this by setting the Mode explicitly on the binding in the DataTemplate like so?

    <TextBox Text="{Binding Name, Mode=OneWay}" />

    Thanks,
    Unni


    This posting is provided "AS IS" with no warranties, and confers no rights.
    Friday, October 15, 2010 5:01 PM
  • I know that I'm exposing my Model to View. I've read as much as I can about MVVM and found that this approach is also used and it it open to discussion. Right now I see this usage type much more effective for my needs. I want to keep my Model to notify property notifications and also the logic on these property changes. And also duplicating so many properties of Model on also ViewModel is not easy and hard to maintain. And as of right now (until I start to use Blend :) ) I haven't seen any downside of this approach and therefore I don't consider quitting it.

    Thanks for the advice but I hope it will be fixed as soon as possible.

    Will I get any updates about this error?

    Friday, October 15, 2010 5:33 PM