Editing currently selected item in datagrid - c#

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;
}
}
}

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
After changing start temperature to 40, the datagrid value also has been changed.
Let's create an error in start temperature text box.
And now the other one
You can see now both the properties are validated separately.
This is the project I have created.
Project Structure
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();
}
}

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).
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;
}
}
}

Related

DataContext binding in UserControl WPF

Trying to get DataContext in UserControl.
My
structure
I have the model Car
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace AutoShop.MVVM.Model
{
class Car : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
private string _Model;
public string Model
{
get
{
return _Model;
}
set
{
_Model = value;
OnPropertyChanged();
}
}
private string _Mark;
public string Mark
{
get
{
return _Mark;
}
set
{
_Mark = value;
OnPropertyChanged();
}
}
private float _Volume;
public float Volume
{
get
{
return _Volume;
}
set
{
_Volume = value;
OnPropertyChanged();
}
}
private int _DateOfIssue;
public int DateOfIssue
{
get
{
return _DateOfIssue;
}
set
{
_DateOfIssue = value;
OnPropertyChanged();
}
}
public enum EngineTypes {
DISEL,
PETROL
};
private EngineTypes _EngineType;
public EngineTypes EngineType
{
get
{
return _EngineType;
}
set
{
_EngineType = value;
OnPropertyChanged();
}
}
private string _ImageUrl;
public string ImageUrl
{
get
{
return _ImageUrl;
}
set
{
_ImageUrl = value;
OnPropertyChanged();
}
}
public Car()
{
}
}
}
And I have main view model
using AutoShop.MVVM.Model;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace AutoShop.MVVM.ViewModel
{
class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string prop = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
public HomeViewModel HomeVM;
private object _CurrentPage;
public object CurrentPage
{
get
{
return _CurrentPage;
}
set
{
_CurrentPage = value;
OnPropertyChanged();
}
}
private List<Car> _Cars;
public List<Car> Cars
{
get
{
return _Cars;
}
set
{
_Cars = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
Cars = new List<Car>() {
new Car
{
Mark = "Audi",
Model = "asdf",
Volume = 1.4F,
DateOfIssue = 2019,
EngineType = Car.EngineTypes.DISEL,
ImageUrl = "img/img"
},
new Car
{
Mark = "Moto",
Model = "asdf",
Volume = 1.4F,
DateOfIssue = 2019,
EngineType = Car.EngineTypes.DISEL,
ImageUrl = "img/img"
},
new Car
{
Mark = "Some",
Model = "asdf",
Volume = 1.4F,
DateOfIssue = 2019,
EngineType = Car.EngineTypes.DISEL,
ImageUrl = "img/img"
}
};
HomeVM = new HomeViewModel();
CurrentPage = HomeVM;
}
}
}
I need to display cars on the ProductPage.xaml and I do it by the next way
<UserControl x:Class="AutoShop.MVVM.View.ProductPage"
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:AutoShop.MVVM.View"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
>
<StackPanel Background="#fff">
<WrapPanel>
<Grid Width="200px" Margin="30 0 0 0">
<TextBox x:Name="field4" Tag="C:\Users\user\Desktop\Learning\Весенний семестр\ООТП\AutoShop\AutoShop\Images\u.png" Style="{StaticResource TextBoxTemplate}" />
<TextBlock IsHitTestVisible="False" Text="Марка" Padding="20 10 0 0">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="FontFamily" Value="/Fonts/#Montserrat Light" />
<Setter Property="Foreground" Value="#ccc" />
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=field4}" Value="">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Grid Width="200px" Margin="30 0 0 0">
<TextBox x:Name="field7" Tag="C:\Users\user\Desktop\Learning\Весенний семестр\ООТП\AutoShop\AutoShop\Images\u.png" Style="{StaticResource TextBoxTemplate}" />
<TextBlock IsHitTestVisible="False" Text="username" Padding="20 10 0 0">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="FontFamily" Value="/Fonts/#Montserrat Light" />
<Setter Property="Foreground" Value="#ccc" />
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=field7}" Value="">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Grid Width="200px" Margin="30 0 0 0">
<ComboBox Style="{StaticResource ComboBoxTheme}" SelectedIndex="0">
<ComboBoxItem>
<TextBlock Text="asdasdasd" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="fsdfsd" />
</ComboBoxItem>
</ComboBox>
</Grid>
</WrapPanel>
<Grid Height="400">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl x:Name="ItemsWrapper" ItemsSource="{Binding Cars}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="10" BorderBrush="Black" BorderThickness="2" Height="279">
<Grid Height="279" Width="200">
<Image Source="{Binding Path=Image}" Width="100" Height="100" VerticalAlignment="Top" Margin="0 10 0 0" />
<TextBlock Text="{Binding Path=Name, StringFormat='Name: {0}'}" VerticalAlignment="Top" Margin="0,120,0,0"/>
<TextBlock Text="{Binding Path=Mark, StringFormat='Rating: {0}' }" VerticalAlignment="Top" Margin="0 180 0 0" />
<TextBlock Text="{Binding Path=Model, StringFormat='Category: {0}'}" VerticalAlignment="Top" Margin="0,200,0,0" />
<TextBlock Text="{Binding Path=Volume, StringFormat='Price: {0}'}" VerticalAlignment="Top" Margin="0,160,0,0" />
<TextBlock Text="{Binding Path=DateOfIssue, StringFormat='Description: {0}'}" VerticalAlignment="Top" Margin="0,140,0,0" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</StackPanel>
</UserControl>
And it the MainForm.xaml I added ProductPage.xaml
<ContentControl Margin="10" Grid.Column="1" Content="{Binding CurrentPage}"/>
The problem is that nothing is being outputted, I think it might be due to the loss of context. How do I properly pass the context to UserControl (ProductPage.xaml)?
ProductPage.xaml
Update:
I set DataContext for MainWindow
And DataContext working becouse {Binding CurrentPage} is working, but binding on Cars field is not working
Add this to the Window.Resources. Of course DataContext must be set for the MainForm.
<Window>
<Window.Resources>
<DataTemplate DataType="{x:Type xmlnsForVewModel:MainViewModel}">
<ProductPage/>
</DataTemplate>
</Window.Resources>
</Window>
and if for ProductPage HomeViewModel supposed to be a DataContext, then HomeViewModel has to have Cars property, not MainViewModel.
1.find the MainForm.xaml.cs
2.find ctor
public MainForm()
{
InitializeComponent();
//set the MainForm DataContext
DataContext = new MainViewModel();
}
3.you should add ContentControl.DataTemplate to show your HomeViewModel
----20210427 update
Here demo i make, project name is WpfApp4
MainWindow.xaml
<Window x:Class="WpfApp4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp4"
xmlns:views="clr-namespace:WpfApp4.Views"
xmlns:viewModels="clr-namespace:WpfApp4.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<viewModels:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding ItemsSource, Mode=OneWay}" SelectedItem="{Binding SelectedCarViewModel, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Column="1">
<ContentControl Content="{Binding SelectedCarViewModel}">
<ContentControl.ContentTemplate>
<!--Style your product page here-->
<DataTemplate DataType="{x:Type viewModels:CarViewModel}">
<UniformGrid Columns="2">
<TextBlock Text="Name"/>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="Volume"/>
<TextBlock Text="{Binding Volume}"/>
<TextBlock Text="Price"/>
<TextBlock Text="{Binding Price}"/>
</UniformGrid>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace WpfApp4
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
MainWindowViewModel.cs
using System.Collections.ObjectModel;
namespace WpfApp4.ViewModels
{
class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<CarViewModel> ItemsSource { get; } = new ObservableCollection<CarViewModel>();
private CarViewModel selectedCarViewModel;
public CarViewModel SelectedCarViewModel
{
get { return this.selectedCarViewModel; }
set { SetProperty(ref this.selectedCarViewModel, value); }
}
public MainWindowViewModel()
{
ItemsSource.Add(new CarViewModel() { Name = "BMW", Volume = 10, Price = 100 });
ItemsSource.Add(new CarViewModel() { Name = "Toyota", Volume = 5, Price = 80 });
ItemsSource.Add(new CarViewModel() { Name = "Benz", Volume = 20, Price = 150 });
// don't let view binding null value, so here initialze property "SelectedCarViewModel"
this.selectedCarViewModel = ItemsSource[0];
}
}
}
CarViewModel.cs
namespace WpfApp4.ViewModels
{
class CarViewModel : ViewModelBase
{
private string name;
public string Name
{
get { return this.name; }
set { SetProperty(ref this.name, value); }
}
private float volume;
public float Volume
{
get { return this.volume; }
set { SetProperty(ref this.volume, value); }
}
private decimal price;
public decimal Price
{
get { return this.price; }
set { SetProperty(ref this.price, value); }
}
}
}
ViewModelBase.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
namespace WpfApp4.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T t, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(t, value))
{
return false;
}
else
{
t = value;
RaisePropertyChanged(propertyName);
return true;
}
}
}
}

WPF UserControl - Why is the value not propagating?

This user control I am developing was working and now it has quit working and I cannot figure out what changed. (I've closed and re-opened VS2019 several times, it shows no errors when building or running.) For clarity, I've included the pertinent code sections below but I've included all the code at bottom.
MessagePanel.xaml.cs
public string MessageResponse
{
get { return (string)GetValue(MessageResponseProperty); }
set { SetValue(MessageResponseProperty, value); }
}
// Using a DependencyProperty as the backing store for MessageResponse. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MessageResponseProperty =
DependencyProperty.Register("MessageResponse", typeof(string), typeof(MessagePanel), new PropertyMetadata(""));
private void ButtonProceed_Click(object sender, RoutedEventArgs e)
{
MessageResponse = "Proceed";
}
private void ButtonHalt_Click(object sender, RoutedEventArgs e)
{
MessageResponse = "Halt";
}
MessagePanel.xaml
<TextBox Text="{Binding MessageResponse, RelativeSource={RelativeSource AncestorType=UserControl}}" />
MainWindow.xaml
<uc:MessagePanel MessageResponse="{Binding MainMessageResponse}" />
MainVM.cs
private string mainMessageResponse;
public string MainMessageResponse
{
get => mainMessageResponse;
set
{
mainMessageResponse = value;
NotifyPropertyChanged();
}
}
As far as I can tell, the DependencyProperty MessageResponse in MessagePanel should be propagated to the MainMessageResponse property in the view model MainVM.cs. Certainly, if I insert code in the view model to set the MainMessageResponse value, the NotifyPropertyChanged() fires and the value appears in the bound TextBox in MainWindow.xaml. But when I click on either button of the user control, though the value appears in the bound TextBox in MessagePanel.xaml, the value no longer propagates through to MainMessageResponse.
What am I missing here?
Full code follows (stripped to the bare necessities):
MessagePanel.xaml
<UserControl x:Class="Common.UserControls.MessagePanel"
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"
mc:Ignorable="d"
d:DesignHeight="100" d:DesignWidth="400"
Visibility="Visible" >
<Grid>
<Border
MinWidth="50" MinHeight="50"
Background="LightCoral"
BorderBrush="Black"
BorderThickness="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<StackPanel>
<StackPanel Width="400" Margin="5,5,5,0">
<TextBox x:Name="Title" TextWrapping="Wrap" MinHeight="16" Background="LightPink" HorizontalAlignment="Center" />
<TextBox x:Name="Message" IsReadOnly="True" TextWrapping="Wrap" MinHeight="42" Background="LightPink" HorizontalAlignment="Stretch" />
</StackPanel>
<DockPanel >
<CheckBox x:Name="CheckBoxConfirm" Checked="CheckBoxConfirm_Checked" Unchecked="CheckBoxConfirm_Unchecked" FontStyle="Italic" FontWeight="Bold" HorizontalAlignment="Center" />
</DockPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="ButtonProceed" Click="ButtonProceed_Click" Width="50" Margin="5,5" />
<Button x:Name="ButtonHalt" Click="ButtonHalt_Click" Width="50" Margin="5,5" />
</StackPanel>
<TextBox Visibility="Visible" Name="Response" Text="{Binding MessageResponse, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
</Border>
</Grid>
</UserControl>
MesssagePanel.xaml.cs
using System.Windows;
using System.Windows.Controls;
namespace Common.UserControls
{
/// <summary>
/// Interaction logic for MessagePanel.xaml
/// </summary>
public partial class MessagePanel : UserControl
{
public enum MessageType
{
Ok,
OkCancel,
YesNo
}
public MessagePanel()
{
InitializeComponent();
Title.Text = "This is a title";
Message.Text = "This is a test message with title and [Yes] and [No] buttons and requires a confirmation.";
ButtonProceed.Content = "Yes";
ButtonHalt.Content = "No";
CheckBoxConfirm.Visibility = Visibility.Collapsed;
}
#region Dependeny Properties
public string MessageResponse
{
get { return (string)GetValue(MessageResponseProperty); }
set { SetValue(MessageResponseProperty, value); }
}
// Using a DependencyProperty as the backing store for MessageResponse. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MessageResponseProperty =
DependencyProperty.Register("MessageResponse", typeof(string), typeof(MessagePanel), new PropertyMetadata(""));
#endregion
#region Event Handlers
private void ButtonProceed_Click(object sender, RoutedEventArgs e)
{
// User wants to proceed
MessageResponse = "Proceed";
}
private void ButtonHalt_Click(object sender, RoutedEventArgs e)
{
// User wants to not proceed
MessageResponse = "Halt";
}
private void CheckBoxConfirm_Checked(object sender, RoutedEventArgs e)
{
ButtonProceed.IsEnabled = true;
}
private void CheckBoxConfirm_Unchecked(object sender, RoutedEventArgs e)
{
ButtonProceed.IsEnabled = false;
}
#endregion
}
}
MainWindow.xaml
<Window x:Class="WpfUserControl.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="clr-namespace:Common.UserControls;assembly=Common"
xmlns:local="clr-namespace:WpfUserControl"
mc:Ignorable="d"
Title="Demo"
Height="300" Width="600">
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="10" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="10" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="1" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBox Text="{Binding MainMessageResponse}" Width="50" Height="22" />
</StackPanel>
</Grid>
<uc:MessagePanel MessageResponse="{Binding MainMessageResponse}" />
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
using WpfUserControl.ViewModels;
namespace WpfUserControl.Views
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainVM vm = new MainVM();
DataContext = vm;
}
}
}
MainVM.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WpfUserControl.ViewModels
{
public partial class MainVM : INotifyPropertyChanged
{
public MainVM()
{
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
#region MessagePanel
private string mainMessageResponse;
public string MainMessageResponse
{
get => mainMessageResponse;
set
{
mainMessageResponse = value;
NotifyPropertyChanged();
}
}
#endregion
}
}
You should set the Mode of the Binding to TwoWay. You can do this for an individual binding in the XAML markup:
<uc:MessagePanel MessageResponse="{Binding MainMessageResponse, Mode=TwoWay}" />
Or you can specify the default value for all bindings when you register the dependency property in the control:
public static readonly DependencyProperty MessageResponseProperty =
DependencyProperty.Register("MessageResponse", typeof(string), typeof(MessagePanel),
new FrameworkPropertyMetadata("") { BindsTwoWayByDefault = true });

Data-binding to KVP of Object,Object not allowing properties to be used or updated in real time?

I have Operation class and Result class that I use to build a list, I use xaml to bind to the properties and a converter to return some content based on certain properties, Most of this works....
To make it easy I will post the xaml then note what works and what I need help with.
<UserControl x:Class="OperationListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:telerik="http://schemas.telerik.com/2008/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"
mc:Ignorable="d"
xmlns:prism="http://www.codeplex.com/prism"
xmlns:inf="clr-namespace:***;assembly=***"
xmlns:c="clr-namespace:****.***"
d:DesignHeight="300" d:DesignWidth="300">
<Control.Resources>
<c:LanguageTextConverter x:Key="langConverter" />
<c:ResultViewConverter x:Key="statusConverter" />
<c:OpDetailViewConverter x:Key="opConverter" />
</Control.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<telerik:RadGridView Name="OperationGrid"
Grid.Column="0"
RowHeight="75"
SelectionMode="Single"
SelectedItem="{Binding SelectedOperation, Mode=TwoWay}"
ItemsSource="{Binding Operations}"
IsReadOnly="True"
AutoGenerateColumns="False"
RowDetailsVisibilityMode="VisibleWhenSelected"
ShowGroupPanel="False"
RowIndicatorVisibility="Collapsed" >
<telerik:RadGridView.RowDetailsTemplate>
<DataTemplate>
<ContentControl Grid.Column="1" Template="{Binding Converter={StaticResource opConverter}}" />
</DataTemplate>
</telerik:RadGridView.RowDetailsTemplate>
<telerik:RadGridView.Columns>
<telerik:GridViewDataColumn Header="Name"
DataMemberBinding="{Binding Key.operationName, Converter={StaticResource langConverter}}"
Width="2*"
IsGroupable="False" />
<telerik:GridViewDataColumn Header="Result"
Width="1*"
MaxWidth="75">
<telerik:GridViewDataColumn.CellTemplate>
<DataTemplate>
<Grid>
<ContentControl Template="{Binding Value, Converter={StaticResource statusConverter}}"/>
</Grid>
</DataTemplate>
</telerik:GridViewDataColumn.CellTemplate>
</telerik:GridViewDataColumn>
</telerik:RadGridView.Columns>
</telerik:RadGridView>
</Grid>
everything up to the final binding works, and even the final binding works to a point...
<ContentControl Template="{Binding Value, Converter={StaticResource statusConverter}}"/>
this works, when binding to Value the converter fires on load and loads the appropriate xaml.
However no additional changes update this value...
In the code behind at some point the Value.status is updated , the setter on the status property fires an propertyChanged. but nothing is caught on the front end.
public OverallStatus status
{
get { return this.Status; }
set { this.Status = value; onPropertyChanged(this, "status"); }
}
What is the property syntax to bind to a Value.Property (currently doesn't work at all) AND have it recognize the propertyChanged setter 3 levels down from the view.
I have created a sample showing nested properties. When I change a property value at button click, it is reflected in the grid. More can be read here :
Property path syntax
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="524.627" Width="862.313">
<StackPanel>
<DataGrid x:Name="Dgrd" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Value.Status.Name}" ClipboardContentBinding="{x:Null}" Header="Status"/>
</DataGrid.Columns>
</DataGrid>
<Button Content="Button" HorizontalAlignment="Left" Margin="29,38,0,0" Grid.Row="1" VerticalAlignment="Top" Width="75" Click="Button_Click"/>
</StackPanel>
</Window>
using System;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Dgrd.ItemsSource = DataStore.Operations;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
DataStore.Operations[0].Value.Status.Name = "N/A"; // change first item
}
}
public class DataStore
{
//static List<Operation> _operations;
public static List<Operation> Operations;
static DataStore()
{
Operations =
new List<Operation>() {
new Operation(){ Value = new RecordInner(){ Status = new OverallStatus (){ Name="Pending"}}},
new Operation(){ Value = new RecordInner(){ Status = new OverallStatus (){ Name="Started"}}},
new Operation(){ Value = new RecordInner(){ Status = new OverallStatus (){ Name="Completed"}}}
};
}
}
public class Operation
{
RecordInner _value;
public RecordInner Value
{
get { return _value; }
set { _value = value; }
}
}
public class RecordInner
{
OverallStatus _status;
public OverallStatus Status
{
get { return _status; }
set { _status = value; }
}
}
public class OverallStatus:INotifyPropertyChanged
{
string _name;
public string Name { get { return _name; } set { _name = value; OnPropertyChanged("name"); } }
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}

Insert Rows in WPF

I want to create the game OXO on WPF. I added the columns in my DataGrid but I want to add the rows without a value. When the user click on the cell, I want that the cell change his value to "X" or "O" depends on the user's turn. Please guide me. How can I manage it ?
This is my grid at the moment :
<Grid >
<DataGrid x:Name="grid"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Height="237" Width="300"
Margin="27,23,0,0" AutoGenerateColumns="False" FrozenColumnCount="3" CanUserDeleteRows="False" CanUserResizeRows="False" CanUserAddRows="False"
>
<DataGrid.Columns>
<DataGridTextColumn Binding="{x:Null}" ClipboardContentBinding="{x:Null}" Width="100"/>
<DataGridTextColumn Binding="{x:Null}" ClipboardContentBinding="{x:Null}" Width="100"/>
<DataGridTextColumn Binding="{x:Null}" ClipboardContentBinding="{x:Null}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
Thank you guys !
You can use 3 rows and 3 columns in a grid and use checkboxes as content. And then you can style the Checkbox so that it displays X or O on toggle. Below is a sample xaml that I quickly wrote. Please let me know if it helps
<Grid Width="200" Height="200" ShowGridLines="True">
<Grid.Resources>
<Style TargetType="{x:Type CheckBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid Background="Transparent">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="txt" Foreground="Black"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="ToggleButton.IsChecked" Value="True">
<Setter Property="Text" TargetName="txt" Value="X"/>
</Trigger>
<Trigger Property="ToggleButton.IsChecked" Value="False">
<Setter Property="Text" TargetName="txt" Value="O"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="0" Grid.Column="1"/>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="0" Grid.Column="2"/>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="1" Grid.Column="0"/>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="1" Grid.Column="1"/>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="1" Grid.Column="2"/>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="2" Grid.Column="0"/>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="2" Grid.Column="1"/>
<CheckBox Content="RadioButton" IsChecked="{x:Null}" Grid.Row="2" Grid.Column="2"/>
</Grid>
DataGrid is not the right choice for this requirement.
I suggest laying out 9 buttons (with empty content) in a regular Grid and handling the click events from there. After a click, simply change the button content to X or O.
OK here is an example that i have knocked up, #Gusdor i did use buttons in the end BUT not 9 of them!!....
The example checks for winning lines and stops the game as soon as one player has won, you then press the reset button to start again....
Start a new wpf project, call it noughtsandcrosses....Paste the following code into your MainWindow.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:noughtsandcrosses" x:Class="noughtsandcrosses.MainWindow"
Title="MainWindow" Height="386.551" Width="270.253">
<Window.Resources>
<local:GameOverToEnabledConverter x:Key="GameOverToEnabledConverter" />
</Window.Resources>
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<Grid>
<Button Content="Reset" Command="{Binding ResetClickCommand}" IsEnabled="{Binding GameOver}" Margin="0,272,0,0"/>
<DataGrid SelectionUnit="Cell" ItemsSource="{Binding rows, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" x:Name="grid"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Height="257" Width="240"
Margin="10,10,0,0" AutoGenerateColumns="False" FrozenColumnCount="3" CanUserDeleteRows="False" CanUserResizeRows="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTemplateColumn Width="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button IsEnabled="{Binding DataContext.GameOver, Converter={StaticResource GameOverToEnabledConverter}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" Content="{Binding Cell1.State}" Command="{Binding DataContext.ButtonClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" CommandParameter="{Binding Cell1}" FontSize="50" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Height="75"></Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button IsEnabled="{Binding DataContext.GameOver, Converter={StaticResource GameOverToEnabledConverter}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" Content="{Binding Cell2.State}" Command="{Binding DataContext.ButtonClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" CommandParameter="{Binding Cell2}" FontSize="50" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Height="75"></Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Width="75">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button IsEnabled="{Binding DataContext.GameOver, Converter={StaticResource GameOverToEnabledConverter}, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" Content="{Binding Cell3.State}" Command="{Binding DataContext.ButtonClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:MainWindow}}}" CommandParameter="{Binding Cell3}" FontSize="50" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Height="75"></Button>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Create a new class called RelayCommand.cs and paste the following code into it....
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace noughtsandcrosses
{
public class RelayCommand<T> : ICommand
{
#region Fields
readonly Action<T> _execute = null;
readonly Predicate<T> _canExecute = null;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of <see cref="DelegateCommand{T}"/>.
/// </summary>
/// <param name="execute">Delegate to execute when Execute is called on the command. This can be null to just hook up a CanExecute delegate.</param>
/// <remarks><seealso cref="CanExecute"/> will always return true.</remarks>
public RelayCommand(Action<T> execute)
: this(execute, null)
{
}
/// <summary>
/// Creates a new command.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
public RelayCommand(Action<T> execute, Predicate<T> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion
#region ICommand Members
///<summary>
///Defines the method that determines whether the command can execute in its current state.
///</summary>
///<param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
///<returns>
///true if this command can be executed; otherwise, false.
///</returns>
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute((T)parameter);
}
///<summary>
///Occurs when changes occur that affect whether or not the command should execute.
///</summary>
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
///<summary>
///Defines the method to be called when the command is invoked.
///</summary>
///<param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to <see langword="null" />.</param>
public void Execute(object parameter)
{
_execute((T)parameter);
}
#endregion
}
}
Create a new class called ViewModel.cs and paste the following code into it....
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reflection;
namespace noughtsandcrosses
{
public class ViewModel : INotifyPropertyChanged
{
public RelayCommand<Cell> ButtonClickCommand { get; set; }
public RelayCommand<string> ResetClickCommand { get; set; }
public int NoughtsScore { get; set; }
public int CrossesScore { get; set; }
public int TotalScore { get; set; }
private List<Row> _test;
public List<Row> rows
{
get { return _test; }
set { _test = value; }
}
private Cell.CellState _turn;
public Cell.CellState turn
{
get { return _turn; }
set { _turn = value; }
}
private bool _GameOver;
public bool GameOver
{
get { return _GameOver; }
set { _GameOver = value; NotifyPropertyChanged("GameOver"); }
}
public ViewModel()
{
ButtonClickCommand = new RelayCommand<Cell>(OnButtonClickCommand);
ResetClickCommand = new RelayCommand<string>(OnResetClickCommand);
// Start with Noughts going first
this.turn = Cell.CellState.Noughts;
// Add 3 blank rows
this.rows = new List<Row>();
this.rows.Add(new Row());
this.rows.Add(new Row());
this.rows.Add(new Row());
}
private void OnResetClickCommand(string obj)
{
this.rows.Select(c => { c.Cell1.State = GetEnumDescription(Cell.CellState.None); c.Cell2.State = GetEnumDescription(Cell.CellState.None); c.Cell3.State = GetEnumDescription(Cell.CellState.None); return c; }).ToList();
this.NoughtsScore = 0;
this.CrossesScore = 0;
this.GameOver = false;
this.TotalScore = 0;
}
private void OnButtonClickCommand(Cell obj)
{
bool winner = false;
if (obj.State == null)
{
obj.State = GetEnumDescription(this.turn);
// swap user..... don't like this but for 20min wonder someone else can fix it
if (this.turn == Cell.CellState.Noughts)
{
NoughtsScore += obj.Mask;
this.turn = Cell.CellState.Crosses;
winner = CheckForWinner(NoughtsScore);
}
else
{
CrossesScore += obj.Mask;
this.turn = Cell.CellState.Noughts;
winner = CheckForWinner(CrossesScore);
}
TotalScore += obj.Mask;
}
if (winner || CheckForGameOver(this.TotalScore))
this.GameOver = true;
}
private bool CheckForWinner(int Score)
{
if ((Score & 7) == 7)
return true;
if ((Score & 56) == 56)
return true;
if ((Score & 448) == 448)
return true;
if ((Score & 73) == 73)
return true;
if ((Score & 146) == 146)
return true;
if ((Score & 292) == 292)
return true;
if ((Score & 273) == 273)
return true;
if ((Score & 84) == 84)
return true;
return false;
}
private bool CheckForGameOver(int TotalScore)
{
if ((TotalScore & 511) == 511)
return true;
return false;
}
public static string GetEnumDescription(Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes =
(DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute),
false);
if (attributes != null && attributes.Length > 0)
return attributes[0].Description;
else
return value.ToString();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
public class Row
{
public Cell Cell1 { get; set; }
public Cell Cell2 { get; set; }
public Cell Cell3 { get; set; }
public Row()
{
this.Cell1 = new Cell();
this.Cell2 = new Cell();
this.Cell3 = new Cell();
}
}
public class Cell : INotifyPropertyChanged
{
private static int power = 0;
public enum CellState
{
[Description(null)]
None,
[Description("O")]
Noughts,
[Description("X")]
Crosses
}
private string _State;
public string State
{
get { return _State; }
set { _State = value; NotifyPropertyChanged("State"); }
}
private int _Mask;
public int Mask
{
get { return _Mask; }
set { _Mask = value; }
}
public Cell()
{
this.State = GetEnumDescription(CellState.None);
this.Mask = 1 << power;
power++;
}
public static string GetEnumDescription(Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes =
(DescriptionAttribute[])fi.GetCustomAttributes(
typeof(DescriptionAttribute),
false);
if (attributes != null && attributes.Length > 0)
return attributes[0].Description;
else
return value.ToString();
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
}
create a new class called GameOverToEnabledConverter.cs (simply inverts a bool value to set the button enabled property) and paste the following code into it....
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace noughtsandcrosses
{
class GameOverToEnabledConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return !(bool)value;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return value.Equals(true) ? parameter : Binding.DoNothing;
}
}
}

Binding Validation.HasError property in MVVM

I am currently implementing a ValidationRule to check if some invalid character are in a TextBox. I am happy that setting the class I have implemented that inherits ValidationRule on my TextBox sets it in red when such characters are found, but I would also like to use the Validation.HasError property or the Validation.Errors property to pop a messagebox telling the user that there are errors in the various textboxes in the page.
Is there a way to bind a property in my ViewModel to the Validation.HasError and/or to the Validation.Errors properties in order for me to have access to them in my ViewModel?
Here is my error style for the TextBox:
<Style x:Key="ErrorValidationTextBox" TargetType="{x:Type pres:OneTextBox}">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel LastChildFill="True">
<TextBlock DockPanel.Dock="Right"
Foreground="Red"
FontSize="12pt"
Text="{Binding ElementName=MyAdorner,
Path=AdornedElement.(Validation.Errors)[0].ErrorContent}">
</TextBlock>
<AdornedElementPlaceholder x:Name="MyAdorner"/>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here is how I declare my TextBox (OneTextBox encapsulates the regular WPF TextBox) in my XAML:
<pres:OneTextBox Watermark="Name..." Margin="85,12,0,0" Style="{StaticResource ErrorValidationTextBox}"
AcceptsReturn="False" MaxLines="1" Height="22" VerticalAlignment="Top"
HorizontalAlignment="Left" Width="300" >
<pres:OneTextBox.Text>
<Binding Path="InterfaceSpecification.Name" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<interfaceSpecsModule:NoInvalidCharsRule/>
</Binding.ValidationRules>
</Binding>
</pres:OneTextBox.Text>
</pres:OneTextBox>
The Validation.HasError is readonly property, therefore Binding will not work with this property. This can be seen in ILSpy:
public virtual bool HasError
{
get
{
return this._validationError != null;
}
}
As an alternative, you should see a great article which provides a solution in the form of use attached dependency properties, there you will see a detailed explanation of the example.
Below is a full example from this article, I just translated it under C#, the original language is VB.NET:
XAML
<Window x:Class="HasErrorTestValidation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:HasErrorTestValidation"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:TestData />
</Window.DataContext>
<StackPanel>
<TextBox x:Name="TestTextBox"
local:ProtocolSettingsLayout.MVVMHasError="{Binding Path=HasError}">
<TextBox.Text>
<Binding Path="TestText" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:OnlyNumbersValidationRule ValidatesOnTargetUpdated="True"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock>
<TextBlock.Text>
<Binding Path="HasError" StringFormat="HasError is {0}"/>
</TextBlock.Text>
</TextBlock>
<TextBlock>
<TextBlock.Text>
<Binding Path="(Validation.HasError)" ElementName="TestTextBox" StringFormat="Validation.HasError is {0}"/>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</Window>
Code-behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
#region Model
public class TestData : INotifyPropertyChanged
{
private bool _hasError = false;
public bool HasError
{
get
{
return _hasError;
}
set
{
_hasError = value;
NotifyPropertyChanged("HasError");
}
}
private string _testText = "0";
public string TestText
{
get
{
return _testText;
}
set
{
_testText = value;
NotifyPropertyChanged("TestText");
}
}
#region PropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string sProp)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(sProp));
}
}
#endregion
}
#endregion
#region ValidationRule
public class OnlyNumbersValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var result = new ValidationResult(true, null);
string NumberPattern = #"^[0-9-]+$";
Regex rgx = new Regex(NumberPattern);
if (rgx.IsMatch(value.ToString()) == false)
{
result = new ValidationResult(false, "Must be only numbers");
}
return result;
}
}
#endregion
public class ProtocolSettingsLayout
{
public static readonly DependencyProperty MVVMHasErrorProperty= DependencyProperty.RegisterAttached("MVVMHasError",
typeof(bool),
typeof(ProtocolSettingsLayout),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
null,
CoerceMVVMHasError));
public static bool GetMVVMHasError(DependencyObject d)
{
return (bool)d.GetValue(MVVMHasErrorProperty);
}
public static void SetMVVMHasError(DependencyObject d, bool value)
{
d.SetValue(MVVMHasErrorProperty, value);
}
private static object CoerceMVVMHasError(DependencyObject d,Object baseValue)
{
bool ret = (bool)baseValue;
if (BindingOperations.IsDataBound(d,MVVMHasErrorProperty))
{
if (GetHasErrorDescriptor(d)==null)
{
DependencyPropertyDescriptor desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
desc.AddValueChanged(d,OnHasErrorChanged);
SetHasErrorDescriptor(d, desc);
ret = System.Windows.Controls.Validation.GetHasError(d);
}
}
else
{
if (GetHasErrorDescriptor(d)!=null)
{
DependencyPropertyDescriptor desc= GetHasErrorDescriptor(d);
desc.RemoveValueChanged(d, OnHasErrorChanged);
SetHasErrorDescriptor(d, null);
}
}
return ret;
}
private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor",
typeof(DependencyPropertyDescriptor),
typeof(ProtocolSettingsLayout));
private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
{
var ret = d.GetValue(HasErrorDescriptorProperty);
return ret as DependencyPropertyDescriptor;
}
private static void OnHasErrorChanged(object sender, EventArgs e)
{
DependencyObject d = sender as DependencyObject;
if (d != null)
{
d.SetValue(MVVMHasErrorProperty, d.GetValue(Validation.HasErrorProperty));
}
}
private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
{
var ret = d.GetValue(HasErrorDescriptorProperty);
d.SetValue(HasErrorDescriptorProperty, value);
}
}
As an alternative to the use of ValidationRule, in MVVM style you can try to implement IDataErrorInfo Interface. For more info see this:
Enforcing Complex Business Data Rules with WPF
all perfect work set NotifyOnValidationError="True" on binding;
(or maybe with binding group also possible)
then use
<Button IsEnabled="{Binding ElementName=tbPeriod, Path=(Validation.HasError)}"
sample with one textBox:
<val:RangeRulecan be changed to ms sample agerangerule etc
<TextBox MaxLength="5" x:Name="tbPeriod" HorizontalAlignment="Left" VerticalAlignment="Top" Width="162" Margin="10,10,0,0" Style="{StaticResource TextBoxInError}">
<TextBox.Text>
<Binding Path="ReportPeriod" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
<Binding.ValidationRules>
<val:RangeRule Min="70" Max="5000" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
In response to Anatoliy's request for an example of a non-working project:
Generic.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestAttachedPropertyValidationError">
<Style TargetType="{x:Type local:TextBoxCustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TextBoxCustomControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Grid.Resources>
<Label
Grid.Row ="0"
Grid.Column="0"
Content="Enter a numeric value:" />
<TextBox
Grid.Row ="0"
Grid.Column="2"
local:HasErrorUtility.HasError="{Binding NumericPropHasError, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
Text="{Binding NumericProp, Mode=TwoWay, UpdateSourceTrigger=LostFocus, RelativeSource={RelativeSource TemplatedParent}}" />
<Label
Grid.Row ="1"
Grid.Column="0"
Content="Value entered:" />
<Label
Grid.Row ="1"
Grid.Column="2"
Content="{TemplateBinding NumericProp}" />
<Label
Grid.Row ="2"
Grid.Column="0"
Grid.ColumnSpan="3"
Visibility="{TemplateBinding NumericPropHasError, Converter={StaticResource BooleanToVisibilityConverter}}"
Foreground="Red"
Content="Not a numeric value" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
TextBoxCustomControl.cs
using System.Windows;
using System.Windows.Controls;
namespace TestAttachedPropertyValidationError
{
public class TextBoxCustomControl : Control
{
static TextBoxCustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBoxCustomControl), new FrameworkPropertyMetadata(typeof(TextBoxCustomControl)));
}
public static readonly DependencyProperty NumericPropProperty =
DependencyProperty.Register("NumericProp", typeof (int), typeof (TextBoxCustomControl), new PropertyMetadata(default(int)));
public int NumericProp
{
get { return (int) GetValue(NumericPropProperty); }
set { SetValue(NumericPropProperty, value); }
}
public static readonly DependencyProperty NumericPropHasErrorProperty =
DependencyProperty.Register("NumericPropHasError", typeof (bool), typeof (TextBoxCustomControl), new PropertyMetadata(default(bool)));
public bool NumericPropHasError
{
get { return (bool) GetValue(NumericPropHasErrorProperty); }
set { SetValue(NumericPropHasErrorProperty, value); }
}
}
}
HasErrorUtility.cs
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace TestAttachedPropertyValidationError
{
class HasErrorUtility
{
public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached("HasError",
typeof(bool),
typeof(HasErrorUtility),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
null,
CoerceHasError));
public static bool GetHasError(DependencyObject d)
{
return (bool)d.GetValue(HasErrorProperty);
}
public static void SetHasError(DependencyObject d, bool value)
{
d.SetValue(HasErrorProperty, value);
}
private static object CoerceHasError(DependencyObject d, Object baseValue)
{
var ret = (bool)baseValue;
if (BindingOperations.IsDataBound(d, HasErrorProperty))
{
if (GetHasErrorDescriptor(d) == null)
{
var desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
desc.AddValueChanged(d, OnHasErrorChanged);
SetHasErrorDescriptor(d, desc);
ret = Validation.GetHasError(d);
}
}
else
{
if (GetHasErrorDescriptor(d) != null)
{
var desc = GetHasErrorDescriptor(d);
desc.RemoveValueChanged(d, OnHasErrorChanged);
SetHasErrorDescriptor(d, null);
}
}
return ret;
}
private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor",
typeof(DependencyPropertyDescriptor),
typeof(HasErrorUtility));
private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
{
var ret = d.GetValue(HasErrorDescriptorProperty);
return ret as DependencyPropertyDescriptor;
}
private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
{
d.SetValue(HasErrorDescriptorProperty, value);
}
private static void OnHasErrorChanged(object sender, EventArgs e)
{
var d = sender as DependencyObject;
if (d != null)
{
d.SetValue(HasErrorProperty, d.GetValue(Validation.HasErrorProperty));
}
}
}
}
ViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace TestAttachedPropertyValidationError
{
public class ViewModel :INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _vmNumericProp;
private bool _vmNumericPropHasError;
public int VmNumericProp
{
get { return _vmNumericProp; }
set
{
_vmNumericProp = value;
OnPropertyChanged();
}
}
public bool VmNumericPropHasError
{
get { return _vmNumericPropHasError; }
set
{
_vmNumericPropHasError = value;
OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
MainWindow.xaml
<Window x:Class="TestAttachedPropertyValidationError.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestAttachedPropertyValidationError"
Title="MainWindow" Height="350" Width="525">
<StackPanel Margin="10">
<StackPanel.Resources>
<local:ViewModel x:Key="VM1"/>
<local:ViewModel x:Key="VM2"/>
</StackPanel.Resources>
<Label Content="Custom Control...}"></Label>
<local:TextBoxCustomControl
Margin="10"
DataContext="{StaticResource VM1}"
NumericProp="{Binding VmNumericProp}"
NumericPropHasError="{Binding VmNumericPropHasError}"/>
<Label Content="Regular XAML...}" Margin="0,20,0,0"/>
<Grid
Margin="10"
DataContext="{StaticResource VM2}"
>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<Grid.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Grid.Resources>
<Label
Grid.Row ="0"
Grid.Column="0"
Content="Enter a numeric value:" />
<TextBox
Grid.Row ="0"
Grid.Column="2"
local:HasErrorUtility.HasError="{Binding VmNumericPropHasError, Mode=TwoWay}"
Text="{Binding VmNumericProp, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
<Label
Grid.Row ="1"
Grid.Column="0"
Content="Value entered:" />
<Label
Grid.Row ="1"
Grid.Column="2"
Content="{Binding VmNumericProp}" />
<Label
Grid.Row ="2"
Grid.Column="0"
Grid.ColumnSpan="3"
Visibility="{Binding VmNumericPropHasError, Converter={StaticResource BooleanToVisibilityConverter}}"
Foreground="Red"
Content="Not a numeric value" />
</Grid>
</StackPanel>

Categories

Resources