I have a set of converters that are used dynamically position squares on a canvas. The squares are stored in an observable collection. CanvasPositionScaleConverter converts a value within a range to a value between 0 and 1. If the value it is converting is outside the specified range, it will throw an Argument exception.
My issue is when I clear my squares collection, whilst the screen becomes empty, "phantom" elements seem to be left behind which the converters continue to act on. Therefore, when the canvas is resized, I still get exceptions being thrown, even if the squares collection has been cleared.
Why are the converters still running on these elements that have been deleted through Squares.Clear()?
Note
Using Squares = new ObservableCollection<Square>() instead of Squares.Clear() still causes this issue.
Update
This issue does not happen if the Sqaures collection is never added too and Resize is pressed. It only happens if elements have been deleted from the collection (and obviously if haven't been removed from the collection but are outside the valid range).
The exception details are
Exception thrown: 'System.ArgumentException' in LateExceptionMcve.dll
An unhandled exception of type 'System.ArgumentException' occurred in LateExceptionMcve.dll
Value cannot be greater than max
MCVE
MainWindow.xaml
<Window x:Class="LateExceptionMcve.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:LateExceptionMcve"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:MainWindow, IsDesignTimeCreatable=True}"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Viewbox Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="0"
Stretch="Uniform">
<Border BorderThickness="1"
BorderBrush="Black">
<Border.Resources>
<local:CanvasScaleConverter x:Key="CanvasScaleConverter" />
<local:CanvasPositionScaleConverter x:Key="CanvasPositionScaleConverter" />
</Border.Resources>
<ItemsControl ItemsSource="{Binding Squares}"
Width="{Binding Size, Converter={StaticResource CanvasScaleConverter}}"
Height="{Binding Size, Converter={StaticResource CanvasScaleConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left">
<Setter.Value>
<MultiBinding Converter="{StaticResource CanvasPositionScaleConverter}">
<Binding Path="X" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Min" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Max" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Size"
Converter="{StaticResource CanvasScaleConverter}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Canvas.Top">
<Setter.Value>
<MultiBinding Converter="{StaticResource CanvasPositionScaleConverter}">
<Binding Path="Y" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Min" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Max" />
<Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}"
Path="DataContext.Size"
Converter="{StaticResource CanvasScaleConverter}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Width" Value="50" />
<Setter Property="Height" Value="50" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="Red" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Viewbox>
<Button Grid.Column="0"
Grid.Row="1"
Content="Clear"
Click="Clear" />
<Button Grid.Column="1"
Grid.Row="1"
Content="Resize"
Click="Resize" />
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Data;
namespace LateExceptionMcve
{
public partial class MainWindow : INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
Min = 0;
Max = 10;
Size = Max - Min;
Squares = new ObservableCollection<Square>
{
new Square
{
X = 8,
Y = 7,
},
};
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void Resize(object sender, RoutedEventArgs e)
{
Min = 0;
Max = 5;
Size = Max - Min;
}
private void Clear(object sender, RoutedEventArgs e)
{
Squares.Clear();
}
private ObservableCollection<Square> _squares;
public ObservableCollection<Square> Squares
{
get => _squares;
set
{
_squares = value;
OnPropertyChanged();
}
}
private double _size;
public double Size
{
get => _size;
set
{
_size = value;
OnPropertyChanged();
}
}
private double _min;
public double Min
{
get => _min;
set
{
_min = value;
OnPropertyChanged();
}
}
private double _max;
public double Max
{
get => _max;
set
{
_max = value;
OnPropertyChanged();
}
}
}
public sealed class Square : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private double _x;
public double X
{
get => _x;
set
{
_x = value;
OnPropertyChanged();
}
}
private double _y;
public double Y
{
get => _y;
set
{
_y = value;
OnPropertyChanged();
}
}
}
public sealed class CanvasScaleConverter : IValueConverter
{
private const double Scale = 100;
public object Convert(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
if (value is double valueToScale)
{
return valueToScale * Scale;
}
return value;
}
public object ConvertBack(
object value,
Type targetType,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
public sealed class CanvasPositionScaleConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values.Length == 4 &&
values[0] is double value &&
values[1] is double min &&
values[2] is double max &&
values[3] is double canvasWidthHeight)
{
return canvasWidthHeight * RangeToNormalizedValue(min, max, value);
}
return values;
}
private static double RangeToNormalizedValue(
double min,
double max,
double value)
{
if (min > max)
{
throw new ArgumentException("Min cannot be less than max");
}
if (value < min)
{
throw new ArgumentException("Value cannot be less than min");
}
if (value > max)
{
throw new ArgumentException("Value cannot be greater than max");
}
return (value - min) / (max - min);
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
}
Edit
I have filled a bug report on the WPF GitHub repo, so hopefully that may help find what is going on here.
Information from miloush on the GitHub issue.
The cause of this issue is that even though the Squares collection has been cleared and the objects are no longer visible on the screen, the objects have not been garbage collected.
It is possible to force the garbage collection to happen by modifying the Clear method to something like
private void Clear(object sender, RoutedEventArgs e)
{
Squares.Clear();
Dispatcher.BeginInvoke(new Action(GC.Collect), DispatcherPriority.ContextIdle);
}
And this will (eventually) clear the objects away. However, it is not immediate. Even if just GC.Collect() after Squares.Clear had been called it is not immediate.
This issue could occur at many times in WPF, including in common actions such as virtualisation. Therefore it would be better to update the converter so it can handle invalid inputs gracefully.
public sealed class CanvasPositionScaleConverter : IMultiValueConverter
{
public object Convert(
object[] values,
Type targetType,
object parameter,
CultureInfo culture)
{
if (values.Length != 4 ||
!(values[0] is double value) ||
!(values[1] is double min) ||
!(values[2] is double max) ||
!(values[3] is double canvasWidthHeight))
{
return null;
}
try
{
return canvasWidthHeight * RangeToNormalizedValue(min, max, value);
}
catch (ArgumentException e) when (e.Message == ValueLessThanMin)
{
return 0;
}
catch (ArgumentException e) when (e.Message == ValueGreaterThanMax)
{
return canvasWidthHeight;
}
}
// This is in a utility class normally
private const string ValueLessThanMin = "Value cannot be less than min";
private const string ValueGreaterThanMax = "Value cannot be greater than max";
private static double RangeToNormalizedValue(
double min,
double max,
double value)
{
if (min > max)
{
throw new ArgumentException("Min cannot be less than max");
}
if (value < min)
{
throw new ArgumentException(ValueLessThanMin);
}
if (value > max)
{
throw new ArgumentException(ValueGreaterThanMax);
}
return (value - min) / (max - min);
}
public object[] ConvertBack(
object value,
Type[] targetTypes,
object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
Related
When I add my usercontrol to a window, why does the text vibrates in design mode?.
<UserControl x:Class="AircraftGauges.InnerGauge"
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:AircraftGauges"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="200"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<local:CenterConverter x:Key="CenterConverter"/>
</UserControl.Resources>
<Canvas Width="200" Height="200" Background="White">
<Path Stroke="Black" StrokeThickness="1" Data="M 40, 95 V 105 H 160 V 95 H 105 V 85 H 95 V 95 Z " Fill="Black"/>
<TextBlock Name="CenterLabel" Text="{Binding PathData}" Foreground="Black" VerticalAlignment="Center"
Canvas.Left="100" Canvas.Top="112">
<TextBlock.Margin>
<MultiBinding Converter="{StaticResource CenterConverter}">
<Binding ElementName="CenterLabel" Path="ActualWidth"/>
<Binding ElementName="CenterLabel" Path="ActualHeight"/>
</MultiBinding>
</TextBlock.Margin>
</TextBlock>
</Canvas>
</UserControl>
Code Behind: InnerGauge.xaml.cs:
namespace AircraftGauges
{
/// <summary>
/// Interaction logic for InnerGauge.xaml
/// </summary>
public partial class InnerGauge : UserControl, INotifyPropertyChanged
{
private string _path;
public event PropertyChangedEventHandler PropertyChanged;
public InnerGauge()
{
InitializeComponent();
SizeChanged += SizeChangedEventHandler;
}
private void SizeChangedEventHandler(object sender, SizeChangedEventArgs e)
{
UpdatePathData(e.NewSize.Width, e.NewSize.Height);
}
private void UpdatePathData(double w, double h)
{
var newPath = $"W {w} -- H {h}";
if (String.CompareOrdinal(_path, newPath) != 0)
{
_path = newPath;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PathData)));
}
}
public string PathData
{
get
{
return _path;
}
}
}
}
CenterConverter.cs
namespace AircraftGauges
{
public class CenterConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue)
{
return DependencyProperty.UnsetValue;
}
var width = (double) values[0];
var height = (double) values[1];
return new Thickness(-width/2, -height/2, 0, 0);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Interesting case indeed, and the reason is layout rounding. It seems that wpf designer by default sets UseLayoutRounding to true for the window you view in it. However, in runtime UseLayoutRounding is false for window. What layout rounding means is things like width\height\margins are rounded to whole pixel during layout measurment. In your CenterConverter you calculate margin of textblock based on it's own height and width. But changing margin causes width change, and that in turn causes margin change. Because of layout rounding this process never ends.
You can verify that by setting UseLayoutRounding = false explicitly (designer will stop vibrating) or setting to true explicitly (will vibrate in runtime). In general you need to fix CenterConverter to take that into account.
In a view in my application I have the following definition for a DataGridTemplateColumn:
<DataGridTemplateColumn Header="Date and Time" Width="Auto" SortMemberPath="ModuleInfos.FileCreationDateTime">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Width="16" Height="16" ToolTip="Original date changed!"
Source="pack://application:,,,/UI.Resources;component/Graphics/InformationImage.png">
<Image.Visibility>
<MultiBinding Converter="{converters:MultiDateTimeConverter}">
<Binding Path="ModuleInfos.FileCreationDateTime" UpdateSourceTrigger="PropertyChanged"/>
<Binding Path="PartListInfos.ModuleDateTime" UpdateSourceTrigger="PropertyChanged"/>
</MultiBinding>
</Image.Visibility>
</Image>
<TextBlock Grid.Column="1" Style="{StaticResource DataGridTextBlockStyle}">
<TextBlock.Text>
<MultiBinding Converter="{converters:MultiDateTimeConverter}">
<Binding Path="ModuleInfos.FileCreationDateTime" UpdateSourceTrigger="PropertyChanged"/>
<Binding Path="PartListInfos.ModuleDateTime" UpdateSourceTrigger="PropertyChanged"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
The MultiDateTimeConverter is:
internal class MultiDateTimeConverter : MarkupExtension, IMultiValueConverter
{
private static MultiDateTimeConverter converter;
private const string dateFormatString = "dd.MM.yyyy - HH:mm:ss";
public MultiDateTimeConverter()
{
}
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values != null && values.Length == 2 && values[0] is DateTime)
{
if (values[1] is DateTime)
{
if(targetType == typeof(string))
return ((DateTime) values[1]).ToString(dateFormatString);
if (targetType == typeof (Visibility))
return Visibility.Visible;
}
if(targetType == typeof(string))
return ((DateTime)values[0]).ToString(dateFormatString);
if (targetType == typeof (Visibility))
return Visibility.Collapsed;
}
return values;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return converter ?? (converter = new MultiDateTimeConverter());
}
}
My problem is the SortMemberPath of the DataGridTemplateColumn. In the past the content of the column was just bound to the property ModuleInfos.FileCreationDateTime. But now the content depends on the two properties ModuleInfos.FileCreationDateTime and PartListInfos.ModuleDateTime.
What do I have to do to enable the sorting to the correct values?
Add sort-descriptions for as many properties as you want to DataGrid's ItemsSource. Below code sorts by two properties if you click on FileName column header. Use e.Handled = true to stop default sorting behavior.
Conceptually do this :
private void Dgrd_Sorting(object sender, DataGridSortingEventArgs e)
{
DataGridColumn col = e.Column;
if (col.Header.ToString() == "FileName")
{
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(Dgrd.ItemsSource);
view.SortDescriptions.Add(new System.ComponentModel.SortDescription("FileName", System.ComponentModel.ListSortDirection.Ascending));
view.SortDescriptions.Add(new System.ComponentModel.SortDescription("CreatedBy", System.ComponentModel.ListSortDirection.Ascending));
view.Refresh();
e.Handled = true;
}
else
e.Handled = true;
}
I want to do Binding to specific property, and make the checkbox converter according to the property values in class.
I have an error.
This is my class:
namespace WpfApplication2
{
class Point
{
public int point { get; set; }
public Point(int x)
{
this.point = x;
}
}
}
This is my Converter:
namespace WpfApplication2
{
public class NumberToCheckedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((int)parameter >= 5)
return true;
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
}
This is my CS window's code:
namespace WpfApplication2
public partial class MainWindow : Window
{
List<Point> points;
public MainWindow()
{
InitializeComponent();
points = new List<Point>();
Random rnd = new Random();
for (int i = 0; i < 10; i++)
{
points.Add(new Point(rnd.Next()));
}
this.DataContext = points;
}
}
}
And this is the xaml:
Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:NumberToCheckedConverter x:Key="NumberToCheckedConverter"></local:NumberToCheckedConverter>
<DataTemplate x:Key="MyDataTemplate"
DataType="local:MyData">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70" />
<ColumnDefinition Width="70" />
</Grid.ColumnDefinitions>
<TextBox Text="Over 5" />
<CheckBox Grid.Column="1" IsChecked="{Binding point, Converter={StaticResource NumberToCheckedConverter}, ConverterParameter=point}" IsEnabled="False" />
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemTemplate="{StaticResource MyDataTemplate}" ItemsSource="{Binding}" Height="172" HorizontalAlignment="Left" Margin="0,51,-0.2,0" Name="listBox1" VerticalAlignment="Top" Width="517" >
</ListBox>
</Grid>
I have an error with the converter. What's wrong here?
A ConverterParameter is not a binding, so writing:
IsChecked="{Binding point, Converter={StaticResource NumberToCheckedConverter}, ConverterParameter=point}"
Is setting parameter to "point"; not really what you want. As it turns out, Converter Parameters aren't even Dependency Properties, and so cannot be bound.
However, you don't even need the parameter; just change your code to:
public class NumberToCheckedConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((int)value >= 5)
return true;
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Binding.DoNothing; //Null would cause an error on a set back.
}
}
Converting the value will do what you want. If you wanted the threshold to be configurable, that is where ConverterParamater would come into play.
I have a problem with binding properties.
I have combobox, 4 textboxes, a slider and a class with 4 decimal properties - every property is binded to one textbox. But I have a problem with a slider - depending on combobox I want the slider to be binded to second or fourth property.
Every time I needed binding until now, I could easily do it in XAML, however this time I don't think it's possible.
Ok, so my class [EDITED]:
class Joint : INotifyPropertyChanged
{
private decimal _alfa;
public decimal alfa
{
get { return _alfa; }
set { _alfa = value; OnPropertyChanged(); }
}
private decimal _l;
public decimal l
{
get { return _l; }
set { _l = value; OnPropertyChanged(); }
}
private decimal _lambda;
public decimal lambda
{
get { return _lambda; }
set { _lambda = value; OnPropertyChanged(); }
}
private decimal _theta;
public decimal theta
{
get { return _theta; }
set { _theta = value; OnPropertyChanged(); }
}
private decimal _min;
public decimal min
{
get { return _min; }
set { _min = value; OnPropertyChanged(); }
}
private decimal _max;
public decimal max
{
get { return _max; }
set { _max = value; OnPropertyChanged(); }
}
private TypeOfJoints _type;
public TypeOfJoints type
{
get { return _type; }
set { _type = value; OnPropertyChanged(); }
}
public Joint()
{
alfa = 1.00M;
l = 2.00M;
lambda = 3.00M;
theta = 140.00M;
min = -200.00M;
max = 200.00M;
type = TypeOfJoints.Rotary;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChangedEventHandler propertyChangedEvent = PropertyChanged;
if (propertyChangedEvent != null)
propertyChangedEvent(this, new PropertyChangedEventArgs(propertyName));
}
}
My enum[EDITED]:
enum TypeOfJoints
{
Rotary,
Sliding,
}
And the part of my XAML code[EDITED]:
<GroupBox Header="Joint 1">
<StackPanel DataContext="{StaticResource ResourceKey=joint1}">
<ComboBox SelectedItem="{Binding type, Mode=TwoWay}" ItemsSource="{Binding Source={StaticResource JointEnum}}"/>
<TextBox Text="{Binding alfa, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding l, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding lambda, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding theta, UpdateSourceTrigger=PropertyChanged}"/>
<Slider x:Name="slider1" Minimum="{Binding min}" Maximum="{Binding max}" ValueChanged="Slider_ValueChanged">
<Slider.Resources>
<local:SliderValueConverter x:Key="SliderValueConverter" />
</Slider.Resources>
<Slider.Value>
<MultiBinding Converter="{StaticResource SliderValueConverter}">
<Binding Path="type"/>
<Binding Path="lambda"/>
<Binding Path="theta"/>
</MultiBinding>
</Slider.Value>
</Slider>
</StackPanel>
</GroupBox>
And the Validator class:
class SliderValueConverter :IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
TypeOfJoints type = (TypeOfJoints)values.FirstOrDefault();
Console.WriteLine(type);
if (type == TypeOfJoints.Sliding)
return values.ElementAtOrDefault(1);
else if (type == TypeOfJoints.Rotary)
return values.ElementAtOrDefault(2);
return DependencyProperty.UnsetValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
So now the slider value is binded to property "fourth", but instead, I want this binding to be dependent on ComboBox value - there are two possible: Second and Fourth - the Fourth is the starting value, that's why now I have static binding to property fourth. But I want it to change to value second when the ComboBox value will change.
This is a good usage for MultiBinding. Set it up like this:
<Slider x:Name="slider1" Minimum="{Binding min}" Maximum=" {Binding max}" ValueChanged="Slider_ValueChanged">
<Slider.Resources>
<local:SliderValueConverter x:Key="SliderValueConverter" />
</Slider.Resources>
<Slider.Value>
<MultiBinding Converter="{StaticResource SliderValueConverter}">
<Binding Path="type" />
<Binding Path="second" />
<Binding Path="fourth" />
</MultiBinding>
</Slider.Value>
</Slider>
Note: make sure to use TwoWay binding for ComboBox.SelectedValue so that the view model property will update:
<ComboBox SelectedItem="{Binding type,Mode=TwoWay}" ItemsSource="{Binding Source={StaticResource NumberEnum}}"/>
Lastly, implement SliderValueConverter as an IMultiValueConverter, and return the appropriate value.
public class SliderValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
vartype = values.FirstOrDefault() as TypeOfJoints?;
decimal? val1 = values.ElementAtOrDefault(1) as decimal?,
val2 = values.ElementAtOrDefault(2) as decimal?;
if (type.HasValue && val1.HasValue && val2.HasValue)
{
if (type.Value == TypeOfJoints.Sliding)
return val1.Value;
else if (type.Value == TypeOfJoints.Rotary)
return val2.Value
}
return DependencyProperty.UnsetValue; // no match - return default;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Edit
Another problem: for bindings from a view model source to update, the view model must implement INotifyPropertyChanged, and raise the PropertyChanged event as appropriate. In this case, we would need:
class Joint : INotifyPropertyChanged
{
public decimal first
{
get { return _first; }
set { _first = value; OnPropertyChanged(); }
}
private decimal _first;
// and so forth with the other properties ...
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChangedEventHandler propertyChangedEvent = PropertyChanged;
if (propertyChangedEvent != null)
propertyChangedEvent(this, new PropertyChangedEventArgs(propertyName));
}
}
I'm using a System.Windows.Controls.DataGrid (WPF, .NET 4.0, C#). If a cell's validation fails (HasErrors == TRUE) the OK button should be gray (IsEnabled = FALSE).
The DataGrid validation is performed using a ValidationRule.
I've read several closely related articles here on StackOverflow, but I'm still jammed. I think the problem is that the Validation is on the DataGridRow, but the OK button's IsEnabled binding is looking at the whole grid.
To see the error, add a third row to the grid, and put in an invalid number (e.g. 200).) Or just edit one of the two stock values to be invalid (less than 0, non-integer, or greater than 100).
Here is the xaml:
<Window x:Class="SimpleDataGridValidation.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:SimpleDataGridValidation"
Title="MainWindow"
Height="350"
Width="525">
<Window.Resources>
<c:BoolToOppositeBoolConverter x:Key="boolToOppositeBoolConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<DataGrid Name="myDataGrid"
ItemsSource="{Binding}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Time"
x:Name="TimeField">
<DataGridTextColumn.Binding>
<Binding Path="Time"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<c:TimeValidationRule />
</Binding.ValidationRules>
</Binding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
<DataGrid.RowValidationErrorTemplate>
<ControlTemplate>
<Grid Margin="0,-2,0,-2"
ToolTip="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}},
Path=(Validation.Errors)[0].ErrorContent}">
<Ellipse StrokeThickness="0"
Fill="Red"
Width="{TemplateBinding FontSize}"
Height="{TemplateBinding FontSize}" />
<TextBlock Text="!"
FontSize="{TemplateBinding FontSize}"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</DataGrid.RowValidationErrorTemplate>
</DataGrid>
<Button Name="btnOk"
Width="40"
Content="_OK"
IsDefault="True"
Grid.Row="1"
Click="btnOk_Click"
IsEnabled="{Binding ElementName=myDataGrid, Path=(Validation.HasError), Converter={StaticResource boolToOppositeBoolConverter}}">
</Button>
</Grid>
</Window>
And here is the C# code-behind:
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace SimpleDataGridValidation
{
public partial class MainWindow : Window
{
public ObservableCollection<CTransition> Transitions;
public MainWindow()
{
InitializeComponent();
Transitions = new ObservableCollection<CTransition>();
Transitions.Add(new CTransition(10, 5));
Transitions.Add(new CTransition(20, 7));
myDataGrid.DataContext = Transitions;
}
private void btnOk_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
}
public class CTransition
{
public int Time { get; set; }
public int Speed { get; set; }
public CTransition()
{ }
public CTransition(int thetime, int thespeed)
{
Time = thetime;
Speed = thespeed;
}
}
public class TimeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
if (value != null)
{
int proposedValue;
if (!int.TryParse(value.ToString(), out proposedValue))
{
return new ValidationResult(false, "'" + value.ToString() + "' is not a whole number.");
}
if (proposedValue > 100)
{
return new ValidationResult(false, "Maximum time is 100 seconds.");
}
if (proposedValue < 0)
{
return new ValidationResult(false, "Time must be a positive integer.");
}
}
// Everything OK.
return new ValidationResult(true, null);
}
}
[ValueConversion(typeof(Boolean), typeof(Boolean))]
public class BoolToOppositeBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(bool) && targetType != typeof(System.Nullable<bool>))
{
throw new InvalidOperationException("The target must be a boolean");
}
if (null == value)
{
return null;
}
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(bool) && targetType != typeof(System.Nullable<bool>))
{
throw new InvalidOperationException("The target must be a boolean");
}
if (null == value)
{
return null;
}
return !(bool)value;
}
}
}
I got it to work by adding 3 things:
"NotifyOnValidationError = True" in DataGridTextColumn's Binding
AddHandler(Validation.ErrorEvent, new RoutedEventHandler(OnErrorEvent)); - Added to MainWindow's constructor.
OnErrorEvent function that keeps track of the error counts and sets the OK button's IsEnabled status. - See Disable/enable button with DataGridTextColumn validation and Using WPF Validation rules and disabling a 'Save' button