3

Hei,

I'm creating simple application with MVVM and stumbled on a problem which i find hard to resolve. On my application i have datagrid and couple of controls to edit currently selected item in datagrid. In my ViewModel i have CurrentSequence property what holds ColorSettingsSequencesSequence object (collection of these objects are used as DataContext for datagrid).

Here's xaml:

<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Path=ColorSettingsSequences}"
                  SelectedItem="{Binding Path=CurrentSequence, Mode=TwoWay}">
    .... more things here ...
</DataGrid>

<StackPanel Grid.Column="0" Grid.Row="0">
    <Grid>
        <Label Content="Start temperature (°C)" Height="28" HorizontalAlignment="Left" x:Name="lblSeqStartTemp" VerticalAlignment="Top" />
        <TextBox Height="23" Margin="0,28,10,0" x:Name="tbSeqStartTemp" VerticalAlignment="Top" Text="{Binding Path=CurrentSequence.StartTemp}" />
    </Grid>
    <Grid>
        <Label Content="Start color" Height="28" HorizontalAlignment="Left" x:Name="lblSeqHue" VerticalAlignment="Top" />
        <xctk:ColorPicker Margin="0,28,10,0" x:Name="clrpSeqHue" SelectedColor="{Binding Path=CurrentSequence.StartHue, Converter={StaticResource hueToColor}, ConverterParameter=False}" ShowStandardColors="False" />
    </Grid>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="0">
    <Grid>
        <Label Content="End temperature (°C)" Height="28" HorizontalAlignment="Left" x:Name="lblSeqEndTemp" VerticalAlignment="Top" />
        <TextBox Height="23" Margin="0,28,10,0" x:Name="tbSeqEndTemp" VerticalAlignment="Top" Text="{Binding Path=CurrentSequence.EndTemp}" />
    </Grid>
    <Grid>
        <Label Content="End color" Height="28" HorizontalAlignment="Left" x:Name="lblSeqEndHue" VerticalAlignment="Top" />
        <xctk:ColorPicker Margin="0,28,10,0" x:Name="clrpSeqEndHue" SelectedColor="{Binding Path=CurrentSequence.EndHue, Converter={StaticResource hueToColor}, ConverterParameter=False}" ShowStandardColors="False" />
    </Grid>
</StackPanel>

Code:

private ColorSettingsSequencesSequence _currentSequence;
public ColorSettingsSequencesSequence CurrentSequence
{
    get
    {
        return this._currentSequence;
    }
    set
    {
        this._currentSequence = value;
        OnPropertyChanged("CurrentSequence");
    }
}

That works nicely, but the problem comes when i want to add validation. I would like to validate StartTemp and EndTemp separately and give different errors. How would i break up the ColorSettingsSequencesSequence object so that the bindings would also still work eq if i edit one value it gets updated in the datagrid also?

Here's what i tried, i created 2 new properties and added my validation to those:

private String _currentSequenceStartTemp;
public String CurrentSequenceStartTemp
{
    get
    {
        return _currentSequenceStartTemp;
    }
    set
    {
        this._currentSequenceStartTemp = value;
        CurrentSequence.StartTemp = value;
        RaisePropertyChanged("CurrentSequenceStartTemp");
        Validator.Validate(() => CurrentSequenceStartTemp);
        ValidateCommand.Execute(null);
    }
}

private String _currentSequenceEndTemp;
public String CurrentSequenceEndTemp
{
    get
    {
        return _currentSequenceEndTemp;
    }
    set
    {
        this._currentSequenceEndTemp = value;
        CurrentSequence.EndTemp = value;
        RaisePropertyChanged("CurrentSequenceEndTemp");
        Validator.Validate(() => CurrentSequenceEndTemp);
        ValidateCommand.Execute(null);
    }
}

And the i just binded TextBoxes to those values, instead of binding them straight to CurrentSequence. I also added setting the CurrentSequence values in the setters and hoped that way my changes will be pushed all the way back to original collection and will be changed in datagrid. That didn't happen.. When CurrentSequence is changed i change values of these properties also:

private ColorSettingsSequencesSequence _currentSequence;
public ColorSettingsSequencesSequence CurrentSequence
{
    get
    {
        return this._currentSequence;
    }
    set
    {
        this._currentSequence = value;
        RaisePropertyChanged("CurrentSequence");
        if (value != null)
        {
            CurrentSequenceStartTemp = value.StartTemp;
            CurrentSequenceEndTemp = value.EndTemp;
        }
        else
        {
            CurrentSequenceStartTemp = String.Empty;
            CurrentSequenceEndTemp = String.Empty;
        }
    }
}
hs2d
  • 6,027
  • 24
  • 64
  • 103
  • Okay so your changes are not being pushed to the original collection yeah? Is that the only issue? – Daniel Wardin Mar 02 '13 at 11:28
  • Basically yes, i need to break up the object so i can bind properties of that object to a controls individually so that my validations would work. Maybe my approach is totally flawed and not correct, so i hope someone can tell me whats the right way doing this. – hs2d Mar 02 '13 at 11:33
  • 1
    Did you try using `RaisePropertyChanged("CurrentSequence");` after you change each value in `CurrentSequenceEndTemp` and `CurrentSequenceStartTemp` worth a try? – Daniel Wardin Mar 02 '13 at 12:05
  • @DanielWardin, yes i tried that, didn't help. – hs2d Mar 02 '13 at 12:09
  • 1
    Does your ColorSettingsSequencesSequence implement INotifyPropertyChanged? – makim Mar 06 '13 at 10:18
  • Post your ColorSettingsSequencesSequence implementation please – Alan Mar 06 '13 at 19:16
  • `ColorSettingsSequencesSequence` is a class from model and does not implement INotifyPropertyChanged. So the right way to go would be creating the `ColorSettingsSequencesSequence` class also in ViewModel and use that instead objects straight from model? – hs2d Mar 06 '13 at 19:57

2 Answers2

2

If I have understood correctly, your problem is that you want to commit your property value even if the validation fails. In case I am wrong in this assumption, the solution is even easier, basically what sine hinted at in his comment, that you would only need to implement INotifyPropertyChanged in your ColorSettingsSequencesSequence class.

I couldn't infer from your post what kind of validation you employ, but here is how I'd do it. The key to updating your datagrid even if validation in the textbox fails, is the ValidationStep="UpdatedValue" part of the ValidationRule (and the implementation of the rule).

DemoValidation

View:

<UserControl x:Class="WpfApplication1.DemoValidation"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
                 xmlns:local="clr-namespace:WpfApplication1"
                 mc:Ignorable="d" 
                 d:DesignHeight="300" d:DesignWidth="300">

    <UserControl.DataContext>
        <local:DemoValidationViewModel />
    </UserControl.DataContext>

    <UserControl.Resources>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel>
                            <Border BorderBrush="Red" BorderThickness="1">
                                <AdornedElementPlaceholder Name="ph" />
                            </Border>
                            <Border BorderBrush="LightGray" BorderThickness="1" Background="Beige">
                                <TextBlock Foreground="Red" FontSize="12" Margin="5"
                                           Text="{Binding ElementName=ph, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
                                </TextBlock>
                            </Border>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>

    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="10" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <StackPanel Grid.Column="0" Grid.Row="0">
            <Label Content="Start temperature (°C)" Height="28" HorizontalAlignment="Left" x:Name="lblSeqStartTemp" VerticalAlignment="Top" />
            <TextBox Height="23" x:Name="tbSeqStartTemp" VerticalAlignment="Top" >
                <TextBox.Text>
                    <Binding Path="CurrentSequence.StartTemp" UpdateSourceTrigger="PropertyChanged">
                        <Binding.ValidationRules>
                            <local:TempValidationRule MaximumTemp="400" MinimumTemp="-100" ValidationStep="UpdatedValue" />
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
        </StackPanel>
        <StackPanel Grid.Column="2" Grid.Row="0">
            <Label Content="End temperature (°C)" Height="28" HorizontalAlignment="Left" x:Name="lblSeqEndTemp" VerticalAlignment="Top" />
            <TextBox Height="23" x:Name="tbSeqEndTemp" VerticalAlignment="Top" >
                <TextBox.Text>
                    <Binding Path="CurrentSequence.EndTemp" UpdateSourceTrigger="PropertyChanged" >
                        <Binding.ValidationRules>
                            <local:TempValidationRule MaximumTemp="500" MinimumTemp="100" ValidationStep="UpdatedValue" />
                        </Binding.ValidationRules>
                    </Binding>
                </TextBox.Text>
            </TextBox>
        </StackPanel>

        <DataGrid Grid.Row="2" Grid.ColumnSpan="3" Margin="0,10,0,0"
                      ItemsSource="{Binding Path=ColorSettingsSequences}"
                      SelectedItem="{Binding Path=CurrentSequence, Mode=TwoWay}" />

    </Grid>
</UserControl>

ViewModel:

public class DemoValidationViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    private ColorSettingsSequencesSequence _currentSequence;
    public ColorSettingsSequencesSequence CurrentSequence
    {
        get { return this._currentSequence; }
        set
        {
            this._currentSequence = value;
            OnPropertyChanged("CurrentSequence");
        }
    }

    public List<ColorSettingsSequencesSequence> ColorSettingsSequences { get; private set; }

    public DemoValidationViewModel()
    {
        // dummy data
        this.ColorSettingsSequences = new List<ColorSettingsSequencesSequence>()
        {
            new ColorSettingsSequencesSequence() { StartTemp = "10", EndTemp = "20" },
            new ColorSettingsSequencesSequence() { StartTemp = "20", EndTemp = "30" },
            new ColorSettingsSequencesSequence() { StartTemp = "30", EndTemp = "40" }
        };
    }

}

ColorSettingsSequencesSequence:

public class ColorSettingsSequencesSequence : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }

    private string _startTemp;
    public string StartTemp { get { return _startTemp; } set { _startTemp = value; OnPropertyChanged("StartTemp");}}

    private string _endTemp;
    public string EndTemp { get { return _endTemp; } set { _endTemp = value; OnPropertyChanged("EndTemp"); } }
}

ValidationRule (see also this thread):

public class TempValidationRule : ValidationRule
{
    // default values
    private int _minimumTemp = -273;
    private int _maximumTemp = 2500;

    public int MinimumTemp
    {
        get { return _minimumTemp; }
        set { _minimumTemp = value; }
    }

    public int MaximumTemp
    {
        get { return _maximumTemp; }
        set { _maximumTemp = value; }
    }

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        string error = null;
        string s = GetBoundValue(value) as string;

        if (!string.IsNullOrEmpty(s))
        {
            int temp;
            if (!int.TryParse(s, out temp))
                error = "No valid integer";
            else if (temp > this.MaximumTemp)
                error = string.Format("Temperature too high. Maximum is {0}.", this.MaximumTemp);
            else if (temp < this.MinimumTemp)
                error = string.Format("Temperature too low. Minimum is {0}.", this.MinimumTemp);
        }

        return new ValidationResult(string.IsNullOrEmpty(error), error);

    }

    private object GetBoundValue(object value)
    {
        if (value is BindingExpression)
        {
            // ValidationStep was UpdatedValue or CommittedValue (validate after setting)
            // Need to pull the value out of the BindingExpression.
            BindingExpression binding = (BindingExpression)value;

            // Get the bound object and name of the property
            string resolvedPropertyName = binding.GetType().GetProperty("ResolvedSourcePropertyName", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null).ToString();
            object resolvedSource = binding.GetType().GetProperty("ResolvedSource", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null);

            // Extract the value of the property
            object propertyValue = resolvedSource.GetType().GetProperty(resolvedPropertyName).GetValue(resolvedSource, null);

            return propertyValue;
        }
        else
        {
            return value;
        }
    }
}
Community
  • 1
  • 1
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
2

I have reproduced your problem. But I couldn't find any problem. Everything works fine.

  • Validate StartTemp and EndTemp separately.
  • If one value is updated, the datagrid should also be updated

So I have solved above two problems in my project.

The Results

enter image description here

After changing start temperature to 40, the datagrid value also has been changed.

enter image description here

Let's create an error in start temperature text box.

enter image description here

And now the other one

enter image description here

You can see now both the properties are validated separately.

This is the project I have created.

Project Structure

enter image description here

ViewModelBase class

public class ViewModelBase : INotifyPropertyChanged
{
    #region Implementation of INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
    {
        var handler = PropertyChanged;
        if (handler != null)
            handler(this, args);
    }

    #endregion
}

ColorSettingsSequencesSequence class

public class ColorSettingsSequencesSequence : ViewModelBase, IDataErrorInfo
{
    #region Declarations

    private string startColor;
    private string startTemperature;
    private string endTemperature;

    #endregion

    #region Properties

    /// <summary>
    /// Gets or sets the start color.
    /// </summary>
    /// <value>
    /// The start color.
    /// </value>
    public string StartColor
    {
        get
        {
            return this.startColor;
        }
        set
        {
            this.startColor = value;
            OnPropertyChanged("StartColor");
        }
    }

    /// <summary>
    /// Gets or sets the start temperature.
    /// </summary>
    /// <value>
    /// The start temperature.
    /// </value>
    public string StartTemperature
    {
        get
        {
            return this.startTemperature;
        }
        set
        {
            this.startTemperature = value;
            OnPropertyChanged("StartTemperature");
        }
    }

    /// <summary>
    /// Gets or sets the end temperature.
    /// </summary>
    /// <value>
    /// The end temperature.
    /// </value>
    public string EndTemperature
    {
        get
        {
            return this.endTemperature;
        }
        set
        {
            this.endTemperature = value;
            OnPropertyChanged("EndTemperature");
        }
    }

    #endregion

    /// <summary>
    /// Gets an error message indicating what is wrong with this object.
    /// </summary>
    /// <returns>An error message indicating what is wrong with this object. The default is an empty string ("").</returns>
    public string Error 
    {
        get 
        {
            return "";
        } 
    }

    /// <summary>
    /// Gets the error message for the property with the given name.
    /// </summary>
    /// <param name="columnName">Name of the column.</param>
    /// <returns></returns>
    public string this[string columnName]
    {
        get 
        {
            if (columnName.Equals("StartTemperature"))
            {
                if (string.IsNullOrEmpty(this.StartTemperature))
                {
                    return "Please enter a start temperature";
                }
            }

            if (columnName.Equals("EndTemperature"))
            {
                if (string.IsNullOrEmpty(this.EndTemperature))
                {
                    return "Please enter a end temperature";
                }
            }

            return "";
        }
    }
}

MainViewModel

public class MainViewModel : ViewModelBase
{
    #region Declarations

    private ColorSettingsSequencesSequence currentSequence;
    private ObservableCollection<ColorSettingsSequencesSequence> colorSettingsSequences;

    #endregion

    #region Properties

    /// <summary>
    /// Gets or sets the current sequence.
    /// </summary>
    /// <value>
    /// The current sequence.
    /// </value>
    public ColorSettingsSequencesSequence CurrentSequence
    {
        get
        {
            return this.currentSequence;
        }
        set
        {
            this.currentSequence = value;
            OnPropertyChanged("CurrentSequence");
        }
    }

    /// <summary>
    /// Gets or sets the color settings sequences.
    /// </summary>
    /// <value>
    /// The color settings sequences.
    /// </value>
    public ObservableCollection<ColorSettingsSequencesSequence> ColorSettingsSequences
    {
        get
        {
            return this.colorSettingsSequences;
        }
        set
        {
            this.colorSettingsSequences = value;
            OnPropertyChanged("ColorSettingsSequences");
        }
    }

    #endregion

    #region Commands

    #endregion

    #region Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="MainViewModel" /> class.
    /// </summary>
    public MainViewModel()
    {
        this.ColorSettingsSequences = new ObservableCollection<ColorSettingsSequencesSequence>();

        ColorSettingsSequencesSequence sequence1 = new ColorSettingsSequencesSequence();
        sequence1.StartColor = "Blue";
        sequence1.StartTemperature = "10";
        sequence1.EndTemperature = "50";
        ColorSettingsSequences.Add(sequence1);

        ColorSettingsSequencesSequence sequence2 = new ColorSettingsSequencesSequence();
        sequence2.StartColor = "Red";
        sequence2.StartTemperature = "20";
        sequence2.EndTemperature = "60";
        ColorSettingsSequences.Add(sequence2);

        ColorSettingsSequencesSequence sequence3 = new ColorSettingsSequencesSequence();
        sequence3.StartColor = "Yellow";
        sequence3.StartTemperature = "30";
        sequence3.EndTemperature = "70";
        ColorSettingsSequences.Add(sequence3);

        this.CurrentSequence = sequence1;

    }

    #endregion

    #region Private Methods

    #endregion
}

MainWindow.xaml (XAML)

<Window x:Class="DataGridValidation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" 
        Height="350"
        Width="525">

    <Window.Resources>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <Grid Name="mainGrid">

        <Grid.RowDefinitions>
            <RowDefinition Height="149" />
            <RowDefinition Height="73" />
            <RowDefinition Height="123" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="249*" />
        </Grid.ColumnDefinitions>

        <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding ColorSettingsSequences}"
                  SelectedItem="{Binding CurrentSequence}"
                  IsReadOnly="True">

            <DataGrid.Columns>
                <DataGridTextColumn Header="Start Color" Binding="{Binding StartColor}" />
                <DataGridTextColumn Header="End Color" Binding="{Binding StartTemperature}" />
                <DataGridTextColumn Header="End Color" Binding="{Binding EndTemperature}" />
            </DataGrid.Columns>

        </DataGrid>

        <StackPanel Grid.Column="0" Grid.Row="1">
            <Grid>
                <Label Content="Start temperature (°C)" 
                       Height="28" 
                       HorizontalAlignment="Left" 
                       x:Name="lblSeqStartTemp" 
                       VerticalAlignment="Top" />
                <TextBox Height="23" 
                         Margin="10,28,10,0" 
                         x:Name="tbSeqStartTemp" 
                         VerticalAlignment="Top" 
                         Text="{Binding Path=CurrentSequence.StartTemperature, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"/>
            </Grid>
        </StackPanel>
        <StackPanel Grid.Row="2" Margin="0,0,0,43">
            <Grid>
                <Label Content="End temperature (°C)" 
                       HorizontalAlignment="Left"  
                       VerticalAlignment="Top" />
                <TextBox Height="23" 
                         Margin="10,28,10,0" 
                         x:Name="tbSeqEndTemp" 
                         VerticalAlignment="Top" 
                         Text="{Binding Path=CurrentSequence.EndTemperature, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"/>
            </Grid>
        </StackPanel>
    </Grid>
</Window>

MainWindow.xaml.cs (Code behind file)

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        mainGrid.DataContext = new MainViewModel();
    }
}
Haritha
  • 1,498
  • 1
  • 13
  • 35
  • Yes, thats the solution i ended up also with, created the `ColorSettingsSequencesSequence` class again in ViewModel. Before it i used class from Model layer. – hs2d Mar 08 '13 at 09:00
  • @hs2d: I try not to mind, but can you please leave a comment as to why this is different from my solution, posted 3 hours earlier? – Mike Fuchs Mar 08 '13 at 09:39
  • @adabyron, the problem wasnt commiting invalid data. problem was that the data wasnt pushed back to to the original Model and thats why values in datagrid didnt change. – hs2d Mar 08 '13 at 12:53
  • @hs2d: which is exactly what I commented on in the first paragraph of my answer, and showed how to do in the code. – Mike Fuchs Mar 08 '13 at 14:40
  • @hs2d: You needed to implement INotifyPropertyChanged on the ColorSettingsSequencesSequence. At least give sine an upvote on his comment, because that was really your whole problem. – Mike Fuchs Mar 08 '13 at 14:49
  • I wish i could give u both bounty because u both did excellent job. – hs2d Mar 09 '13 at 11:31