Using WPF Triggers to Change Properties of Named Elements in UserControls - c#

Suppose I have a Window or UserControl with a boatload of named elements. I want to change all these elements' property values based on a single property of either my view model or a custom DP on the parent (really doesn't matter which, because I can easily bind the DP to the view model property).
Here is a barebones example:
<Window x:Class="TriggerFun.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:TriggerFun"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Rect1"
Grid.Column="0"
Width="200" Height="200"
Fill="Red"/>
<Rectangle x:Name="Rect2"
Grid.Column="1"
Width="200" Height="200"
Fill="Yellow"/>
<Button Grid.Row="1"
Grid.ColumnSpan="2"
Click="Button_Click">
Swap!
</Button>
</Grid>
using System;
using System.ComponentModel;
using System.Windows;
namespace TriggerFun
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ViewModel ViewModel => this.DataContext as ViewModel;
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.ViewModel.AlternativeLayout = !this.ViewModel.AlternativeLayout;
}
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
bool _alternativeLayout;
public bool AlternativeLayout
{
get => _alternativeLayout;
set
{
_alternativeLayout = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(AlternativeLayout)));
}
}
}
}
What I would like to do in this example is swap the columns of the red and yellow Rectangles when the user clicks the button. (And yes of course I know I can do this in code-behind. I want to do this in pure XAML).
What would make eminent sense to me is if I could do add this to Window:
<Window.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</Window.Triggers>
Well that doesn't work because I get a runtime error, Triggers collection members must be of type EventTrigger. So all triggers that are children of anything other than a Style, DataTemplate, or ControlTemplate I guess have to be EventTriggers? Fine.
So then I try this:
<Window.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Style>
That won't even compile: TargetName property cannot be set on a Style Setter.
I know I can use TargetName in DataTemplate or ControlTemplate Triggers, but when defining UserControl's and Windows of course you usually don't set the DataTemplate but rather just set the child content directly.
The only thing I can do that I know works is take each element that I want to be changed and give it its own inline style with triggers, with the final XAML looking incredibly ugly:
<Rectangle x:Name="Rect1"
Width="200" Height="200"
Fill="Red">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Grid.Column" Value="0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter Property="Grid.Column" Value="1"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<Rectangle x:Name="Rect2"
Width="200" Height="200"
Fill="Yellow">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Grid.Column" Value="1"/>
<Style.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter Property="Grid.Column" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
Obviously this doesn't scale particularly well.
This is SO easy to do with ControlTemplates and DataTemplates but seems next to impossible when making UserControls. Is there something I'm missing?

You could define a ControlTemplate for the window or user control and define the triggers in the template:
<Window.DataContext>
<local:Window23ViewModel />
</Window.DataContext>
<Grid>
<UserControl>
<UserControl.Template>
<ControlTemplate>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
</DataTrigger>
</ControlTemplate.Triggers>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Rect1"
Grid.Column="0"
Width="200" Height="200"
Fill="Red"/>
<Rectangle x:Name="Rect2"
Grid.Column="1"
Width="200" Height="200"
Fill="Yellow"/>
<Button Grid.Row="1"
Grid.ColumnSpan="2"
Click="Button_Click">
Swap!
</Button>
</Grid>
</ControlTemplate>
</UserControl.Template>
</UserControl>
</Grid>

[ContentProperty(nameof(Setters))]
public class ContentTrigger : FrameworkElement
The application of your AttachedProperty can be simplified somewhat.
Let's change the implementation a bit:
[ContentProperty(nameof(Setters))]
public class ContentTrigger : FrameworkElement
{
#region ContentTriggerCollection Triggers dependency property
public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached(
"ShadowTriggers",
typeof(ContentTriggerCollection),
typeof(ContentTrigger),
new FrameworkPropertyMetadata(
null,
OnTriggersChanged));
public static ContentTriggerCollection GetTriggers(DependencyObject obj)
{
var value = (ContentTriggerCollection)obj.GetValue(TriggersProperty);
if (value == null)
SetTriggers(obj, value = new ContentTriggerCollection());
return value;
}
public static void SetTriggers(DependencyObject obj, ContentTriggerCollection value)
{
obj.SetValue(TriggersProperty, value);
}
private static void OnTriggersChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.OldValue is ContentTriggerCollection oldTriggers)
oldTriggers.SetApply(null);
if (args.NewValue is ContentTriggerCollection newTriggers)
newTriggers.SetApply(obj);
}
#endregion
public BindingBase Binding { get; set; }
public object Value { get; set; }
public SetterBaseCollection Setters { get; } = new SetterBaseCollection();
#region object ActualValue dependency property
internal static readonly DependencyProperty ActualValueProperty = DependencyProperty.Register(
"ActualValue",
typeof(object),
typeof(ContentTrigger),
new PropertyMetadata(
(object)null,
(obj, args) =>
{
((ContentTrigger)obj).OnActualValueChanged(args);
}));
private void OnActualValueChanged(DependencyPropertyChangedEventArgs args)
{
if (TestIsTriggered(args.NewValue))
ExecuteTrigger();
else
RestoreValues();
}
#endregion
private bool TestIsTriggered(object newValue)
{
if (newValue is bool b)
return b && (this.Value as string == "True" || this.Value as string == "true") ||
!b && (this.Value as string == "False" || this.Value as string == "false");
else
return object.Equals(this.Value, newValue);
}
public void Apply(DependencyObject obj)
{
if (!(obj is FrameworkElement fe))
return;
_target = fe;
BindingOperations.SetBinding(this, DataContextProperty, new Binding
{
Source = fe,
Path = new PropertyPath(DataContextProperty)
});
BindingOperations.SetBinding(this, ActualValueProperty, this.Binding);
}
private void ExecuteTrigger()
{
if (_target == null || _isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null)
continue;
_originalValues[(targetElem, setter.Property)] = targetElem.GetValue(setter.Property);
targetElem.SetCurrentValue(setter.Property, ResolveSetterValue(setter));
}
_isTriggered = true;
}
private void RestoreValues()
{
if (_target == null || !_isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null ||
// Value changed some other way since trigger?
targetElem.GetValue(setter.Property) != ResolveSetterValue(setter))
continue;
object restoredValue;
if (_originalValues.TryGetValue((targetElem, setter.Property), out restoredValue))
{
targetElem.SetCurrentValue(setter.Property, restoredValue);
}
}
_isTriggered = false;
}
private FrameworkElement GetTargetElement(string name)
{
FrameworkElement targetElem;
if (!_targetElements.TryGetValue(name, out targetElem))
{
targetElem = _target.FindName(name) as FrameworkElement;
if (targetElem != null)
_targetElements[name] = targetElem;
}
return targetElem;
}
private object ResolveSetterValue(Setter setter)
{
if (setter.Value is DynamicResourceExtension dr)
return _target.FindResource(dr.ResourceKey);
return setter.Value;
}
private Dictionary<(FrameworkElement, DependencyProperty), object> _originalValues =
new Dictionary<(FrameworkElement, DependencyProperty), object>();
private Dictionary<string, FrameworkElement> _targetElements = new Dictionary<string, FrameworkElement>();
private bool _isTriggered = false;
private FrameworkElement _target;
}
public class ContentTriggerCollection : Collection<ContentTrigger>
{
public DependencyObject Apply { get; private set; }
public void SetApply(DependencyObject apply)
{
Apply = apply;
foreach (ContentTrigger trigger in this)
{
if (trigger != null)
trigger.Apply(apply);
}
}
protected override void ClearItems()
{
foreach (ContentTrigger trigger in this)
{
if (trigger != null)
trigger.Apply(null);
}
base.ClearItems();
}
protected override void InsertItem(int index, ContentTrigger item)
{
base.InsertItem(index, item);
if (item != null)
item.Apply(Apply);
}
protected override void RemoveItem(int index)
{
if (this[index] is ContentTrigger removeTrigger)
removeTrigger.Apply(null);
base.RemoveItem(index);
}
protected override void SetItem(int index, ContentTrigger item)
{
if (this[index] is ContentTrigger removeTrigger)
removeTrigger.Apply(null);
base.SetItem(index, item);
if (item != null)
item.Apply(Apply);
}
}
<local:ContentTrigger.Triggers>
<local:ContentTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect1" Property="Rectangle.Fill" Value="Green"/>
<Setter TargetName="Rect1" Property="Rectangle.Stroke" Value="Purple"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
<Setter TargetName="Rect2" Property="Rectangle.Fill" Value="Gray"/>
<Setter TargetName="Rect2" Property="Rectangle.Stroke" Value="Black"/>
</local:ContentTrigger>
</local:ContentTrigger.Triggers>
The key change is registering the name "ShadowTriggers", which is different from the name of the Get and Set methods.
This registration does not allow XAML to refer to the AttachedProperty without going through the getter and setter.

Eldhasp gave me an idea:
[ContentProperty(nameof(Setters))]
public class ContentTrigger : FrameworkElement
{
#region ContentTriggerCollection Triggers dependency property
public static readonly DependencyProperty TriggersProperty = DependencyProperty.RegisterAttached(
"Triggers",
typeof(ContentTriggerCollection),
typeof(ContentTrigger),
new FrameworkPropertyMetadata(
null,
OnTriggersChanged));
public static ContentTriggerCollection GetTriggers(DependencyObject obj)
{
return (ContentTriggerCollection)obj.GetValue(TriggersProperty);
}
public static void SetTriggers(DependencyObject obj, ContentTriggerCollection value)
{
obj.SetValue(TriggersProperty, value);
}
private static void OnTriggersChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var triggers = args.NewValue as ContentTriggerCollection;
foreach (var trigger in triggers)
trigger.Apply(obj);
}
#endregion
public BindingBase Binding { get; set; }
public object Value { get; set; }
public SetterBaseCollection Setters { get; } = new SetterBaseCollection();
#region object ActualValue dependency property
internal static readonly DependencyProperty ActualValueProperty = DependencyProperty.Register(
"ActualValue",
typeof(object),
typeof(ContentTrigger),
new PropertyMetadata(
(object)null,
(obj, args) =>
{
((ContentTrigger)obj).OnActualValueChanged(args);
}));
private void OnActualValueChanged(DependencyPropertyChangedEventArgs args)
{
if (TestIsTriggered(args.NewValue))
ExecuteTrigger();
else
RestoreValues();
}
#endregion
private bool TestIsTriggered(object newValue)
{
if (newValue is bool b)
return b && (this.Value as string == "True" || this.Value as string == "true") ||
!b && (this.Value as string == "False" || this.Value as string == "false");
else
return object.Equals(this.Value, newValue);
}
private void Apply(DependencyObject obj)
{
if (!(obj is FrameworkElement fe))
return;
_target = fe;
BindingOperations.SetBinding(this, DataContextProperty, new Binding
{
Source = fe,
Path = new PropertyPath(DataContextProperty)
});
BindingOperations.SetBinding(this, ActualValueProperty, this.Binding);
}
private void ExecuteTrigger()
{
if (_target == null || _isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null)
continue;
_originalValues[(targetElem, setter.Property)] = targetElem.GetValue(setter.Property);
targetElem.SetCurrentValue(setter.Property, ResolveSetterValue(setter));
}
_isTriggered = true;
}
private void RestoreValues()
{
if (_target == null || !_isTriggered)
return;
foreach (var setterBase in this.Setters)
{
if (!(setterBase is Setter setter) || string.IsNullOrEmpty(setter.TargetName))
continue;
var targetElem = GetTargetElement(setter.TargetName);
if (targetElem == null ||
// Value changed some other way since trigger?
targetElem.GetValue(setter.Property) != ResolveSetterValue(setter))
continue;
object restoredValue;
if (_originalValues.TryGetValue((targetElem, setter.Property), out restoredValue))
{
targetElem.SetCurrentValue(setter.Property, restoredValue);
}
}
_isTriggered = false;
}
private FrameworkElement GetTargetElement(string name)
{
FrameworkElement targetElem;
if (!_targetElements.TryGetValue(name, out targetElem))
{
targetElem = _target.FindName(name) as FrameworkElement;
if (targetElem != null)
_targetElements[name] = targetElem;
}
return targetElem;
}
private object ResolveSetterValue(Setter setter)
{
if (setter.Value is DynamicResourceExtension dr)
return _target.FindResource(dr.ResourceKey);
return setter.Value;
}
private Dictionary<(FrameworkElement, DependencyProperty), object> _originalValues =
new Dictionary<(FrameworkElement, DependencyProperty), object>();
private Dictionary<string, FrameworkElement> _targetElements = new Dictionary<string, FrameworkElement>();
private bool _isTriggered = false;
private FrameworkElement _target;
}
public class ContentTriggerCollection : Collection<ContentTrigger>
{
}
Usage:
<Window x:Class="TriggerFun.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:TriggerFun"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<local:ContentTrigger.Triggers>
<local:ContentTriggerCollection>
<local:ContentTrigger Binding="{Binding AlternativeLayout}" Value="True">
<Setter TargetName="Rect1" Property="Grid.Column" Value="1"/>
<Setter TargetName="Rect1" Property="Rectangle.Fill" Value="Green"/>
<Setter TargetName="Rect1" Property="Rectangle.Stroke" Value="Purple"/>
<Setter TargetName="Rect2" Property="Grid.Column" Value="0"/>
<Setter TargetName="Rect2" Property="Rectangle.Fill" Value="Gray"/>
<Setter TargetName="Rect2" Property="Rectangle.Stroke" Value="Black"/>
</local:ContentTrigger>
</local:ContentTriggerCollection>
</local:ContentTrigger.Triggers>
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Rectangle x:Name="Rect1"
StrokeThickness="2"
Stroke="Blue"
Width="200" Height="200"
Grid.Column="0"
Fill="Red"/>
<Rectangle x:Name="Rect2"
StrokeThickness="2"
Stroke="Orange"
Width="200" Height="200"
Grid.Column="1"
Fill="Yellow"/>
<Button Grid.Row="1"
Grid.ColumnSpan="2"
Click="Button_Click">
Swap!
</Button>
</Grid>
Shame I can't derive from TriggerBase, would be cleaner. But this is as close as I think I can get to what I want.

Related

WPF Dependency Property Not Binding / PropertyChangedCallback Not Getting Called

My project consists of a Treeview with a Datagrid as a child. The parent in the Treeview has a checkbox, and every row in the datagrid also has a check box. My goal is to bind the checkboxes together, so that when the parent checkbox is checked, all of the datagrid rows are checked as well. When the datagrid rows are checked I would like the parent checkbox to be updated based of the datagrid row checked values.
Parent checkbox IsChecked = true when all of the datagrid row checkboxes are true, null if only some are, and false if no rows are checked.
The problem I'm facing is that my dependency properties aren't updating and the PropertyChangedCallback is not getting called.
This is my Checkbox_Helper class in which the IsChecked properties of both the parent and the datagrid row checkboxes are bound to.
public class CheckBox_Helper : DependencyObject
{
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.RegisterAttached("IsChecked", typeof(bool?), typeof(CheckBox_Helper),
new PropertyMetadata(false, new PropertyChangedCallback(OnIsCheckedPropertyChanged)));
private static void OnIsCheckedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Parent && ((bool?)e.NewValue).HasValue)
{
foreach (Child s in (d as Parent).SheetList[0].SheetChildList)
{
if (s.IsSizeCompatible)
{
SetIsChecked(s, (bool?)e.NewValue);
}
}
}
if (d is Child)
{
AddToCheckedElements(d as Child);
int _checked = (d.GetValue(ParentProperty) as Parent).SheetList[0].SheetChildList.Where(x => GetIsChecked(x) == true).Count();
int _unchecked = (d.GetValue(ParentProperty) as Parent).SheetList[0].SheetChildList.Where(x => GetIsChecked(x) == false).Count();
int _total = (d.GetValue(ParentProperty) as Parent).SheetList[0].SheetChildList.Count();
if ((d as Child).MegaParent.SelectedRowList != null && (d as Child).MegaParent.SelectedRowList.Count > 0)
{
(d as Child).MegaParent.SelectedRowList.Where(i => GetIsChecked(i) != GetIsChecked(d as Child)).ToList().ForEach(i => CheckBox_Helper.SetIsChecked(i, GetIsChecked(d as Child)));
}
if (_checked == 0)
{
(d.GetValue(ParentProperty) as Parent).HasCheckedItems = false;
}
else
{
(d.GetValue(ParentProperty) as Parent).HasCheckedItems = true;
}
(d.GetValue(ParentProperty) as Parent).CheckedRatio = $"{_checked}/{_total}";
if (_unchecked > 0 && _checked > 0)
{
CheckBox_Helper.SetIsChecked((d as Child).GetValue(CheckBox_Helper.ParentProperty) as DependencyObject, null);
return;
}
if (_checked > 0)
{
CheckBox_Helper.SetIsChecked((d as Child).GetValue(CheckBox_Helper.ParentProperty) as DependencyObject, true);
return;
}
CheckBox_Helper.SetIsChecked((d as Child).GetValue(CheckBox_Helper.ParentProperty) as DependencyObject, false);
}
}
public static readonly DependencyProperty ParentProperty = DependencyProperty.RegisterAttached("Parent", typeof(Parent), typeof(CheckBox_Helper));
public static void SetIsChecked(DependencyObject element, bool? IsChecked)
{
element.SetValue(CheckBox_Helper.IsCheckedProperty, IsChecked);
}
public static bool? GetIsChecked(DependencyObject element)
{
return (bool?)element.GetValue(CheckBox_Helper.IsCheckedProperty);
}
// Set and Get Parent are unused since they are hardcoded categories
public static void SetParent(DependencyObject element, object Parent)
{
element.SetValue(CheckBox_Helper.ParentProperty, Parent);
}
public static object GetParent(DependencyObject element)
{
return (object)element.GetValue(CheckBox_Helper.ParentProperty);
}
private static void AddToCheckedElements(Child child)
{
ObservableCollection<Child> tempList = child.MegaParent.CheckedElements;
if (!child.MegaParent.CheckedElements.Contains(child) && GetIsChecked(child) == true)
{
tempList.Add(child);
child.MegaParent.CheckedElements = tempList;
}
else if (child.MegaParent.CheckedElements.Contains(child) && GetIsChecked(child) == false)
{
tempList.Remove(child);
child.MegaParent.CheckedElements = tempList;
}
}
}
This is the code for the Parent class:
public class Parent : DependencyObject
{
public string Name { get; set; }
public ObservableCollection<ChildList> SheetList { get; set; }
public static readonly DependencyProperty HasCheckedItemsProperty =
DependencyProperty.Register("HasCheckedItems", typeof(bool), typeof(Parent), new UIPropertyMetadata(false));
public bool HasCheckedItems
{
get { return (bool)GetValue(HasCheckedItemsProperty); }
set { SetValue(HasCheckedItemsProperty, value); }
}
public static readonly DependencyProperty CheckedRatioProperty =
DependencyProperty.Register("CheckedRatio", typeof(string), typeof(Parent), new UIPropertyMetadata($"0/0"));
public string CheckedRatio
{
get { return (string)GetValue(CheckedRatioProperty); }
set { SetValue(CheckedRatioProperty, value); }
}
public Parent(string name)
{
Name = name;
SheetList = new ObservableCollection<ChildList>();
}
}
This is the code for the Child class:
(Note: The MegaParent property is the viewmodel)
public class Child : DependencyObject
{
public ViewSheet Sheet { get; set; }
public string Name { get; set; }
public string Number { get; set; }
private string _paperSize = "";
public string PaperSize
{
get { return _paperSize; }
set { _paperSize = value; }
}
public static readonly DependencyProperty IsSizeCompatibleProperty =
DependencyProperty.Register("IsSizeCompatible", typeof(bool), typeof(Child), new UIPropertyMetadata(true));
public bool IsSizeCompatible
{
get { return (bool)GetValue(IsSizeCompatibleProperty); }
set { SetValue(IsSizeCompatibleProperty, value); }
}
public PrintSheets_ViewModel MegaParent { get; set; }
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool), typeof(Child), new UIPropertyMetadata(false));
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); AddToSelectedList(); }
}
private void AddToSelectedList()
{
ObservableCollection<Child> tempList = MegaParent.SelectedRowList;
if (!MegaParent.SelectedRowList.Contains(this) && (bool)GetValue(IsSelectedProperty) == true)
{
tempList.Add(this);
MegaParent.SelectedRowList = tempList;
}
else if (MegaParent.SelectedRowList.Contains(this) && (bool)GetValue(IsSelectedProperty) == false)
{
tempList.Remove(this);
MegaParent.SelectedRowList = tempList;
}
}
public Child(string name, string number, ViewSheet sheet, string paperSize, bool isSizeCompatible, PrintSheets_ViewModel megaParent)
{
Name = name;
Number = number;
Sheet = sheet;
PaperSize = paperSize;
IsSizeCompatible = isSizeCompatible;
MegaParent = megaParent;
}
}
This is the XAML code for the Treeview:
<Border Grid.Row="1" Grid.RowSpan="4" Grid.Column="3" Grid.ColumnSpan="3" Margin="10,10,10,10"
BorderBrush="Black" BorderThickness="1">
<ScrollViewer x:Name="treeScroller" PreviewMouseWheel="treeScroller_PreviewMouseWheel" VerticalScrollBarVisibility="Auto">
<TreeView Background="LightGray" ItemsSource="{Binding tv_model.ParentItems}" x:Name="sheetTree"
ScrollViewer.CanContentScroll="False">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type m:Parent}" ItemsSource="{Binding SheetList}">
<Border BorderBrush="DarkGray" CornerRadius="5" Padding="5,0,0,0" Height="40"
Width="{Binding Path=ActualWidth, ElementName=sheetTree, Converter={StaticResource minusFortyFiveConverter}}">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding HasCheckedItems}" Value="True">
<Setter Property="Background" Value="#b5ffc0"/>
<Setter Property="BorderThickness" Value="3"/>
</DataTrigger>
<DataTrigger Binding="{Binding HasCheckedItems}" Value="False">
<Setter Property="Background" Value="LightCyan"/>
<Setter Property="BorderThickness" Value="2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"
IsChecked="{Binding Path=(h:CheckBox_Helper.IsChecked), Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
Cursor="Hand"/>
<Label Grid.Column="1" Content="{Binding Name}" VerticalAlignment="Center" HorizontalAlignment="Left" FontWeight="SemiBold"/>
<Label Grid.Column="3" VerticalAlignment="Center" HorizontalContentAlignment="Right"
FontWeight="Normal" FontSize="12"
Content="Sheets Checked:"/>
<Label Grid.Column="4" VerticalAlignment="Center" HorizontalAlignment="Right"
Content="{Binding CheckedRatio}">
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding HasCheckedItems}" Value="True">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="FontSize" Value="14"/>
</DataTrigger>
<DataTrigger Binding="{Binding HasCheckedItems}" Value="False">
<Setter Property="FontWeight" Value="Normal"/>
<Setter Property="FontSize" Value="12"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</Grid>
</Border>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type m:ChildList}">
<DataGrid x:Name="ChildDataGrid" ItemsSource="{Binding SheetChildList}"
Width="{Binding Path=ActualWidth, ElementName=sheetTree, Converter={StaticResource minusEightyConverter}}"
IsReadOnly="True" AutoGenerateColumns="False" SelectionMode="Extended" CanUserResizeColumns="True"
EnableRowVirtualization="True" CanUserAddRows="False" CanUserDeleteRows="False"
CanUserResizeRows="True" CanUserReorderColumns="False" CanUserSortColumns="True" >
<DataGrid.Resources>
<Style TargetType="DataGrid">
<EventSetter Event="LostFocus" Handler="OnLostFocus_DataGrid"/>
</Style>
<Style TargetType="DataGridRow">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsSizeCompatible, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Value="False">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsSizeCompatible, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Value="True">
<Setter Property="IsEnabled" Value="True"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Width="20">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox Cursor="Hand" IsChecked="{Binding Path=(h:CheckBox_Helper.IsChecked), Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Sheet Number" Binding="{Binding Number}" IsReadOnly="True" Width="Auto"/>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" IsReadOnly="True" Width="*"/>
<DataGridTextColumn Header="Paper Size" Binding="{Binding PaperSize}" IsReadOnly="True" Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="Margin" Value="0,3,0,3"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</ScrollViewer>
</Border>
I had the checkboxes working before I added some of the extra methods to the CheckBox_Helper, but I need those methods in order for other controls on my UI to function properly.
I feel like it could be a DataContext issue. I've tried adding RelativeSource to the IsChecked binding with the AncestorType as Datagrid, and I've also tried it as Window.
Still no luck, any help would be greatly appreciated.

WPF ComboBox with checkboxes and textbox with search field

I have gone through this thread
Looking for a WPF ComboBox with checkboxes
and tried to include TextBox in the DataTemplate but not able to view the TextBox on running the application. I have created a UserControl as follows
<UserControl x:Class="MyProj.UserControls.MultiSelectComboBox"
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="300" d:DesignWidth="300">
<ComboBox
x:Name="MultiSelectCombo"
SnapsToDevicePixels="True"
OverridesDefaultStyle="True"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.CanContentScroll="True"
IsSynchronizedWithCurrentItem="True"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
**<TextBlock Text="{Binding Text, Mode=TwoWay}" Tag="{RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}"
Width="100" />**
<CheckBox Content="{Binding Text}"
IsChecked="{Binding Path=IsSelected, Mode=TwoWay}"
Tag="{RelativeSource FindAncestor, AncestorType={x:Type ComboBox}}"
Click="CheckBox_Click" Width="20" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
<ComboBox.Template>
<ControlTemplate TargetType="ComboBox">
<Grid >
<ToggleButton
x:Name="ToggleButton"
Grid.Column="2" IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
Focusable="false"
ClickMode="Press" HorizontalContentAlignment="Left" >
<ToggleButton.Template>
<ControlTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="18"/>
</Grid.ColumnDefinitions>
<Border
x:Name="Border"
Grid.ColumnSpan="2"
CornerRadius="2"
Background="White"
BorderBrush="Silver"
BorderThickness="1,1,1,1" />
<Border
x:Name="BorderComp"
Grid.Column="0"
CornerRadius="2"
Margin="1"
Background="White"
BorderBrush="Black"
BorderThickness="0,0,0,0" >
<TextBlock Text="{Binding Path=Text,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
Background="White" Padding="3" />
</Border>
<Path
x:Name="Arrow"
Grid.Column="1"
Fill="Black"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M 0 0 L 4 4 L 8 0 Z"/>
</Grid>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<Popup
Name="Popup"
Placement="Bottom"
AllowsTransparency="True"
Focusable="False" IsOpen="{TemplateBinding IsDropDownOpen}"
PopupAnimation="Slide">
<Grid
Name="DropDown"
SnapsToDevicePixels="True"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border
x:Name="DropDownBorder"
BorderThickness="1" Background="White"
BorderBrush="Black"/>
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True" DataContext="{Binding}">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" />
</ScrollViewer>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="DropDownBorder" Property="MinHeight" Value="95"/>
</Trigger>
<Trigger SourceName="Popup" Property="Popup.AllowsTransparency" Value="true">
<Setter TargetName="DropDownBorder" Property="CornerRadius" Value="4"/>
<Setter TargetName="DropDownBorder" Property="Margin" Value="0,2,0,0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ComboBox.Template>
</ComboBox>
and here is the code
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MyProj.UserControls
{
/// <summary>
/// Interaction logic for MultiSelectComboBox.xaml
/// </summary>
public partial class MultiSelectComboBox : UserControl
{
private ObservableCollection<Node> _nodeList;
public MultiSelectComboBox()
{
InitializeComponent();
_nodeList = new ObservableCollection<Node>();
}
#region Dependency Properties
public event EventHandler CheckBoxClick;
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(Lexicon<string, int>), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnItemsSourceChanged)));
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(Lexicon<string, int>), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(OnSelectedItemsChanged)));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public static readonly DependencyProperty DefaultTextProperty =
DependencyProperty.Register("DefaultText", typeof(string), typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty));
public Lexicon<string, int> ItemsSource
{
get { return (Lexicon<string, int>)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
}
}
public Lexicon<string, int> SelectedItems
{
get { return (Lexicon<string, int>)GetValue(SelectedItemsProperty); }
set
{
SetValue(SelectedItemsProperty, value);
}
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public string DefaultText
{
get { return (string)GetValue(DefaultTextProperty); }
set { SetValue(DefaultTextProperty, value); }
}
#endregion
#region Events
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox control = (MultiSelectComboBox)d;
control.DisplayInControl();
}
private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MultiSelectComboBox control = (MultiSelectComboBox)d;
control.SelectNodes();
control.SetText();
}
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
CheckBox clickedBox = (CheckBox)sender;
if (clickedBox.Content.ToString() == "All")
{
if (clickedBox.IsChecked.Value)
{
foreach (Node node in _nodeList)
{
node.IsSelected = true;
}
}
else
{
foreach (Node node in _nodeList)
{
node.IsSelected = false;
}
}
}
else
{
int _selectedCount = 0;
foreach (Node s in _nodeList)
{
if (s.IsSelected && s.Text != "All")
_selectedCount++;
}
if (_selectedCount == _nodeList.Count - 1)
_nodeList.FirstOrDefault(i => i.Text == "All").IsSelected = true;
else
_nodeList.FirstOrDefault(i => i.Text == "All").IsSelected = false;
}
SetSelectedItems();
SetText();
if (this.CheckBoxClick != null)
this.CheckBoxClick(this, e);
}
#endregion
#region Methods
private void SelectNodes()
{
foreach (KeyValuePair<string, int> keyValue in SelectedItems)
{
Node node = _nodeList.FirstOrDefault(i => i.Text == keyValue.Key);
if (node != null)
node.IsSelected = true;
}
}
private void SetSelectedItems()
{
if (SelectedItems == null)
SelectedItems = new Lexicon<string, int>();
SelectedItems.Clear();
foreach (Node node in _nodeList)
{
if (node.IsSelected && node.Text != "All")
{
if (this.ItemsSource.Count > 0)
SelectedItems.Add(node.Text, node.Id);
}
}
}
//private void DisplayInControl()
//{
// _nodeList.Clear();
// foreach (KeyValuePair<string, int> keyValue in this.ItemsSource)
// {
// Node node = null;
// if (this.ItemsSource.Count == 1)
// node = new Node(keyValue.Key);
// else
// node = new Node(keyValue.Key);
// _nodeList.Add(node);
// if (this.ItemsSource.Count == 1) SetSelectedItems();
// }
// MultiSelectCombo.ItemsSource = _nodeList;
// if (this.ItemsSource.Count == 1) SetText();
// else
// this.Text = "--Select--";
//}
private void DisplayInControl()
{
_nodeList.Clear();
if (this.ItemsSource.Count > 0)
_nodeList.Add(new Node("All", false, -1));
foreach (KeyValuePair<string, int> keyValue in this.ItemsSource)
{
Node node = new Node(keyValue.Key, false, keyValue.Value);
_nodeList.Add(node);
}
MultiSelectCombo.ItemsSource = _nodeList;
this.Text = "--Select--";
}
private void SetText()
{
if (this.SelectedItems != null)
{
StringBuilder displayText = new StringBuilder();
foreach (Node s in _nodeList)
{
if (s.IsSelected == true && s.Text == "All")
{
displayText = new StringBuilder();
displayText.Append("All");
break;
}
else if (s.IsSelected == true && s.Text != "All")
{
displayText.Append(s.Text);
displayText.Append(',');
}
}
this.Text = displayText.ToString().TrimEnd(new char[] { ',' });
}
// set DefaultText if nothing else selected
if (string.IsNullOrEmpty(this.Text))
{
this.Text = this.DefaultText;
}
}
#endregion
}
public class Node : INotifyPropertyChanged
{
private string _text;
private bool _isSelected;
private int _id;
#region ctor
public Node(string text, bool selected, int id)
{
Text = text;
IsSelected = selected;
Id = id;
}
#endregion
#region Properties
public int Id
{
get
{
return _id;
}
set
{
_id = value;
NotifyPropertyChanged("Id");
}
}
public string Text
{
get
{
return _text;
}
set
{
_text = value;
NotifyPropertyChanged("Text");
}
}
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
_isSelected = value;
NotifyPropertyChanged("IsSelected");
}
}
#endregion
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
So can some one let me know what changes do I need to do for displaying the TextBox on top of the CheckBoxes so that when I enter some text it should filter and give me the results
Modify the ScrollViewer element in the ControlTemplate to include a TextBox:
<Popup
Name="Popup"
Placement="Bottom"
AllowsTransparency="True"
Focusable="False" IsOpen="{TemplateBinding IsDropDownOpen}"
PopupAnimation="Slide">
<Grid
Name="DropDown"
SnapsToDevicePixels="True"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border
x:Name="DropDownBorder"
BorderThickness="1" Background="White"
BorderBrush="Black"/>
<ScrollViewer Margin="4,6,4,6" SnapsToDevicePixels="True" DataContext="{Binding}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox />
<StackPanel Grid.Row="1" IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" />
</Grid>
</ScrollViewer>
</Grid>
</Popup>
You don't need any TextBox in the ItemTemplate.

Bind Button IsEnabled property to DataTemplate items state and one additional condition

I have an object with editable parameters collection which are bound as a ItemsSource to ItemsControl, and a property which checks if all parameter values are ok. This property bound to button's IsEnabled.
I also want to disable the button when any of textbox has validation error (Validation.HasError == true).
Thanks in advance.
XAML:
<Window x:Class="MyWPFTest.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">
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=MyObject.Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}"></TextBlock>
<TextBox Text="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}"></TextBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button IsEnabled="{Binding Path=MyObject.IsParametersOkay}">OK</Button>
</StackPanel>
</Window>
Code:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace MyWPFTest
{
public partial class MainWindow : Window
{
ObjectWithParameters _MyObject = new ObjectWithParameters();
public ObjectWithParameters MyObject { get { return _MyObject; } }
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
}
public class ObjectWithParameters : INotifyPropertyChanged
{
ObservableCollection<Parameter> _Parameters = new ObservableCollection<Parameter>();
public ObservableCollection<Parameter> Parameters { get { return _Parameters; } }
public event PropertyChangedEventHandler PropertyChanged;
public ObjectWithParameters()
{
var p1 = new Parameter("Parameter 1", 0); p1.PropertyChanged += ParameterChanged; Parameters.Add(p1);
var p2 = new Parameter("Parameter 2", 0); p2.PropertyChanged += ParameterChanged; Parameters.Add(p2);
}
void ParameterChanged(object sender, PropertyChangedEventArgs e)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsParametersOkay"));
}
public bool IsParametersOkay
{
get { return Parameters.FirstOrDefault(p => p.Value < 0) == null; }
}
}
public class Parameter : INotifyPropertyChanged
{
double val;
public double Value
{
get { return val; }
set { val = value; if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Value")); }
}
public string Name { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public Parameter(string name, double value) { Name = name; Value = value; }
}
}
Check out MultiTriggers.
<Style.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" Value="#EEEEEE" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasItems" Value="false" />
<Condition Property="Width" Value="Auto" />
</MultiTrigger.Conditions>
<Setter Property="MinWidth" Value="120"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="HasItems" Value="false" />
<Condition Property="Height" Value="Auto" />
</MultiTrigger.Conditions>
<Setter Property="MinHeight" Value="95"/>
</MultiTrigger>
</Style.Triggers>
This is the way I solved the problem. May be it's not a very elegant solution, but it works.
I added a new property IsFormOkay to MainWindow class, which checks both controls and parameters validity. Then I bound Button.IsEnabled to this property and added TextChanged event for TextBox to notify about IsFormOkay.
Here is code added to MainWindow:
public event PropertyChangedEventHandler PropertyChanged;
public bool IsFormOkay { get { return IsValid(Items) && MyObject.IsParametersOkay; } }
public bool IsValid(DependencyObject obj)
{
if (Validation.GetHasError(obj)) return false;
for (int i = 0, n = VisualTreeHelper.GetChildrenCount(obj); i < n; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (!IsValid(child)) return false;
}
return true;
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsFormOkay"));
}
And changes to XAML:
<StackPanel>
<ItemsControl x:Name="Items" ItemsSource="{Binding Path=MyObject.Parameters}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Name}" />
<TextBox TextChanged="TextBox_TextChanged" Text="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button IsEnabled="{Binding Path=IsFormOkay}" Content="OK" />
</StackPanel>

Binding Issues in XAML

I am trying to build a UserControl but I am having trouble getting my bindings to work. I know I am missing something, but I can't figure out what it is. I am not getting any BindingExpressions
XAML
<UserControl x:Class="WpfApplication3.NumericUpDown"
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:WpfApplication3">
<Grid>
<StackPanel>
<TextBox Text="{Binding NumericUpDownText, Mode=TwoWay, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}}}" LostFocus="PART_NumericUpDown_LostFocus">
<TextBox.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding AnyNumericErrors, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}, AncestorLevel=1}}" Value="false">
<Setter Property="Background" Value="Blue" />
</DataTrigger>
<DataTrigger Binding="{Binding AnyNumericErrors, RelativeSource={RelativeSource AncestorType={x:Type local:NumericUpDown}, AncestorLevel=1}}" Value="true">
<Setter Property="Background" Value="Red" />
<Setter Property="ToolTip" Value="There is an error" />
</DataTrigger>
</Style.Triggers>
<EventSetter Event="LostFocus" Handler="PART_NumericUpDown_LostFocus" />
</Style>
</TextBox.Resources>
<TextBox.Template>
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ScrollViewer x:Name="PART_ContentHost"
Grid.Column="0" />
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<RepeatButton x:Name="PART_IncreaseButton"
Click="PART_IncreaseButton_Click"
Content="UP" />
<RepeatButton x:Name="PART_DecreaseButton"
Grid.Row="1"
Click="PART_DecreaseButton_Click"
Content="Down"/>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</TextBox.Template>
</TextBox>
</StackPanel>
</Grid>
C# Code Behind
namespace WpfApplication3
{
/// <summary>
/// Interaction logic for NumericUpDown.xaml
/// </summary>
public partial class NumericUpDown : UserControl
{
public NumericUpDown()
{
InitializeComponent();
//AnyNumericErrors = true;
}
private String _NumericUpDownText;
public String NumericUpDownText
{
get { return _NumericUpDownText; }
set
{
_NumericUpDownText = value;
NotifyPropertyChanged("NumericUpDownText");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private void PART_NumericUpDown_LostFocus(object sender, RoutedEventArgs e)
{
CheckForErrors();
}
public void CheckForErrors()
{
try
{
int value = Int32.Parse(NumericUpDownText);
AnyNumericErrors = false;
}
catch (FormatException)
{
Debug.WriteLine("error");
AnyNumericErrors = true;
}
}
private Boolean m_AnyNumericErrors;
public Boolean AnyNumericErrors
{
get
{
return m_AnyNumericErrors;
}
set
{
m_AnyNumericErrors = value;
NotifyPropertyChanged("AnyNumericErrors");
}
}
#region DP
public Int32 LowerBound
{
get;
set;
}
public static readonly DependencyProperty LowerBoundProperty = DependencyProperty.Register("LowerBound", typeof(Int32), typeof(NumericUpDown));
#endregion
private void PART_IncreaseButton_Click(object sender, RoutedEventArgs e)
{
try
{
Int32 value = Int32.Parse(NumericUpDownText);
value++;
NumericUpDownText = value.ToString();
}
catch (Exception)
{
AnyNumericErrors = true;
}
}
private void PART_DecreaseButton_Click(object sender, RoutedEventArgs e)
{
try
{
Int32 value = Int32.Parse(NumericUpDownText);
value--;
NumericUpDownText = value.ToString();
}
catch (Exception)
{
AnyNumericErrors = true;
}
}
}
}
EDIT
The primary issue is that the DataTriggers are not working... at inception, the textbox is blue, but never changes. And when I press on up/down to increment/decrement the values, the events are getting called, but the value isn't changing in the UI
You should use DependencyProperties like:
public bool AnyNumericErrors
{
get { return (bool)GetValue(AnyNumericErrorsProperty); }
set { SetValue(AnyNumericErrorsProperty, value); }
}
// Using a DependencyProperty as the backing store for AnyNumericErrors. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AnyNumericErrorsProperty =
DependencyProperty.Register("AnyNumericErrors", typeof(bool), typeof(NumericUpDown), new UIPropertyMetadata(false));
If that isn't enough, then remove the level restriction on you ancestor search.

Why does my ConvertBack not getting called, WPF Converter & ValidationRule?

I'm trying to bind a textbox which can validate email addresses split by ',' or ';'.The objective is to have a checkbox, a textbox and a button on the page.If the checkbox is checked, then the user has to enter a valid email address or else the button has to be disabled.If the checdkbox is not clicked then the button has to be enabled.I'm going round in circles with this one, could you please help?
Please find my ViewModel structure below:
public class EmailValidatorViewModel : DependencyObject
{
public EmailValidatorViewModel()
{
OnOkCommand = new DelegateCommand<object>(vm => OnOk(), vm => CanEnable());
OtherRecipients = new List<string>();
}
private bool CanEnable()
{
return !IsChecked || HasOtherRecipients() ;
}
public static readonly DependencyProperty OtherRecipientsProperty =
DependencyProperty.Register("OtherRecipients", typeof(List<string>), typeof(EmailValidatorViewModel));
public List<string> OtherRecipients
{
get { return (List<string>)GetValue(OtherRecipientsProperty); }
set
{
SetValue(OtherRecipientsProperty, value);
}
}
public bool IsChecked { get; set; }
public void OnOk()
{
var count = OtherRecipients.Count;
}
public bool HasOtherRecipients()
{
return OtherRecipients.Count != 0;
}
public DelegateCommand<object> OnOkCommand { get; set; }
}
Also below is my XAML page:
<Window x:Class="EMailValidator.EMailValidatorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:EMailValidator="clr-namespace:EMailValidator" Title="Window1" Height="300" Width="300">
<Window.Resources>
<Style x:Key="ToolTipBound" TargetType="TextBox">
<Setter Property="Foreground" Value="#333333" />
<Setter Property="MaxLength" Value="40" />
<Setter Property="Width" Value="392" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Text" Value=""/>
</Trigger>
</Style.Triggers>
</Style>
<EMailValidator:ListToStringConverter x:Key="ListToStringConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<CheckBox Margin="15,0,0,0" x:Name="OthersCheckbox" Grid.Row="2" Grid.Column="0" Unchecked="OnOthersCheckboxUnChecked" IsChecked="{Binding Path=IsChecked}">Others</CheckBox>
<TextBox Margin="5,0,5,0" x:Name="OtherRecipientsTextBox" Grid.Row="2" Grid.Column="1" IsEnabled="{Binding ElementName=OthersCheckbox, Path=IsChecked}" Style="{StaticResource ToolTipBound}">
<TextBox.Text>
<Binding Path="OtherRecipients" Mode="TwoWay" Converter="{StaticResource ListToStringConverter}" NotifyOnSourceUpdated="True">
<Binding.ValidationRules>
<EMailValidator:EmailValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button x:Name="OkButton" Grid.Row="3" Grid.Column="0" Command="{Binding OnOkCommand}">Ok</Button>
</Grid>
</Window>
Also I do set my datacontext as below in the constructor of my xaml page.
public EMailValidatorWindow()
{
InitializeComponent();
DataContext = new EmailValidatorViewModel();
}
Here is my EMail Validator:
public class EmailValidationRule:ValidationRule
{
private const string EmailRegEx = #"\b[A-Z0-9._%-]+#[A-Z0-9.-]+\.[A-Z]{2,4}\b";
private readonly Regex regEx = new Regex(EmailRegEx,RegexOptions.IgnoreCase);
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var inputAddresses = value as string;
if(inputAddresses == null)
return new ValidationResult(false,"An unspecified error occured while validating the input.Are you sure that you have entered a valid EMail address?");
var list = inputAddresses.Split(new[] {';',','});
var failures = list.Where(item => !regEx.Match(item).Success);
if(failures.Count() <= 0)
return new ValidationResult(true,null);
var getInvalidAddresses = string.Join(",", failures.ToArray());
return new ValidationResult(false,"The following E-mail addresses are not valid:"+getInvalidAddresses+". Are you sure that you have entered a valid address seperated by a semi-colon(;)?.");
}
...and my converter:
public class ListToStringConverter:IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var input = value as List<string>;
if (input.Count == 0)
return string.Empty;
var output = string.Join(";", input.ToArray());
return output;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var input = value as string;
if (string.IsNullOrEmpty(input))
return new List<string>();
var list = input.Split(new[] { ';' });
return list;
}
}
Try setting the UpdateSourceTrigger to 'PropertyChanged' in your textbox binding.
<Binding UpdateSourceTrigger="PropertyChanged" Path="OtherRecipients" Mode="TwoWay" Converter="{StaticResource ListToStringConverter}" NotifyOnSourceUpdated="True">

Categories

Resources