WPF ComboBox with checkboxes and textbox with search field - c#

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.

Related

Using WPF Triggers to Change Properties of Named Elements in UserControls

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.

dynamic property not working well with DependencyProperties

My requirement is Binding dynamic property to my Value (double) property of Custom control.But it not working as expected.
Below is the code :
Simple customcontrol part :
xaml
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<TextBox x:Name="PART_TextBox" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
c#
public class CustomControl1 : Control
{
public double? Value
{
get { return (double?)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
// Using a DependencyProperty as the backing store for Value. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(double?), typeof(CustomControl1), new PropertyMetadata(null, new PropertyChangedCallback(OnValueChanged), new CoerceValueCallback(CoerceValueChange)));
private static object CoerceValueChange(DependencyObject d, object baseValue)
{
return baseValue;
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((CustomControl1)d).OnValueChanged(e);
}
private void OnValueChanged(DependencyPropertyChangedEventArgs e)
{
if (tb != null && e.NewValue != null)
tb.Text = e.NewValue.ToString();
else
tb.Text = string.Empty;
}
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
TextBox tb = null;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
tb = base.GetTemplateChild("PART_TextBox") as TextBox;
}
}
here goes the usage :
xaml
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<Label Content="Double TextBox without Converter" />
<Label Content="Double TextBox with Converter" />
<Label Content="MS TextBox" />
<Button Content="Button" Click="Button_Click_1"/>
</StackPanel>
<StackPanel Grid.Column="1">
<cc:CustomControl1 Value="{Binding MyValue}" Height="40" Width="200" Background="Red"/>
<TextBox Height="30" Text="{Binding MyValue}" />
</StackPanel>
</Grid>
c#
public partial class MainWindow : Window, INotifyPropertyChanged
{
private dynamic myValue;
public dynamic MyValue
{
get { return myValue; }
set { myValue = value; RaisePropertyChanged("MyValue"); }
}
public MainWindow()
{
InitializeComponent();
this.myValue = 3;
this.DataContext = this;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
this.myValue = 5;
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
the coerce's base value is always null,but if i use converter in binding it works as expected.
but i want to use like normal property , Please help me on this.
binding works perfectly if i use TextBox,it is not the issue ,, i'm facing issue while i use DP's in CustomControl
Regards,
Kumar
Found answer from here..............
http://social.msdn.microsoft.com/Forums/vstudio/en-US/7d0f0a90-b0b8-45c9-9ec9-76517c8ea4af/dynamic-property-not-working-well-with-dependencyproperties?forum=wpf

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>

Databound Flipview remove ViewModel.ItemAt(0) causes flip transition

I am making an article reading app (similar to the Bing News app) and I'm using a FlipView to go between the articles. The FlipView has its ItemsSource databound to an ObservableCollection<T> that holds the content of the article.
I only want to keep 3 articles in the ObservableCollection<T> for memory and performance reasons, so I subscribe to the flipView_SelectionChanged event and remove the item at Length - 1 for going back (to the right) and item at 0 for going forward (to the left)
The problem that I'm having is that when I remove the item at 0 after flipping using the touch gesture, the animation plays a second time.
How do I prevent the transition animation from playing a second time?
Here is an example. Create a new Blank Store App, and add the following:
public sealed partial class MainPage : Page
{
public ObservableCollection<int> Items { get; set; }
private Random _random = new Random(123);
public MainPage()
{
this.InitializeComponent();
Items = new ObservableCollection<int>();
Items.Add(1);
Items.Add(1);
Items.Add(1);
flipview.ItemsSource = Items;
}
private void flipview_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (this.flipview.SelectedIndex == 0)
{
Items.Insert(0, 1);
Items.RemoveAt(Items.Count - 1);
}
else if (this.flipview.SelectedIndex == this.flipview.Items.Count - 1)
{
Items.Add(1);
Items.RemoveAt(0);
}
}
}
and this in the .xaml
<Page.Resources>
<DataTemplate x:Key="DataTemplate">
<Grid>
<Grid.Background>
<LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
<GradientStop Color="Black"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Grid.Background>
</Grid>
</DataTemplate>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<FlipView x:Name="flipview"
SelectionChanged="flipview_SelectionChanged"
ItemsSource="{Binding}" ItemTemplate="{StaticResource DataTemplate}"/>
</Grid>
The easiest solution is to use UseTouchAnimationsForAllNavigation so that items manipulation in the view model does not cause the animation to occur at all.
<FlipView UseTouchAnimationsForAllNavigation="False" />
If the animation is important to you, then you can simply bind the value for UseTouchAnimationsForAllNavigation in your view model, like this:
<FlipView UseTouchAnimationsForAllNavigation="{Binding ShowAnimations}" />
Does this make sense? If you do it in your view model (option 2) you can simply disable it while manipulating the collection, and then re-enable it so you get animations.
Here's a sample.
Using this code:
public class MyColorModel : BindableBase
{
Color _Color = default(Color);
public Color Color { get { return _Color; } set { SetProperty(ref _Color, value); } }
Visibility _Selected = Visibility.Collapsed;
public Visibility Selected { get { return _Selected; } set { SetProperty(ref _Selected, value); } }
public event EventHandler RemoveRequested;
public void RemoveMe()
{
if (RemoveRequested != null)
RemoveRequested(this, EventArgs.Empty);
}
public event EventHandler SelectRequested;
public void SelectMe()
{
if (SelectRequested != null)
SelectRequested(this, EventArgs.Empty);
}
}
public class MyViewModel : BindableBase
{
public MyViewModel()
{
this.Selected = this.Colors[1];
foreach (var item in this.Colors)
{
item.RemoveRequested += (s, e) =>
{
this.ShowAnimations = false;
this.Colors.Remove(s as MyColorModel);
this.ShowAnimations = true;
};
item.SelectRequested += (s, e) => this.Selected = s as MyColorModel;
}
}
ObservableCollection<MyColorModel> _Colors = new ObservableCollection<MyColorModel>(new[]
{
new MyColorModel{ Color=Windows.UI.Colors.Red },
new MyColorModel{ Color=Windows.UI.Colors.Green },
new MyColorModel{ Color=Windows.UI.Colors.Yellow },
new MyColorModel{ Color=Windows.UI.Colors.Blue },
new MyColorModel{ Color=Windows.UI.Colors.White },
new MyColorModel{ Color=Windows.UI.Colors.Brown },
new MyColorModel{ Color=Windows.UI.Colors.SteelBlue },
new MyColorModel{ Color=Windows.UI.Colors.Goldenrod },
});
public ObservableCollection<MyColorModel> Colors { get { return _Colors; } }
MyColorModel _Selected = default(MyColorModel);
public MyColorModel Selected
{
get { return _Selected; }
set
{
if (_Selected != null)
_Selected.Selected = Visibility.Collapsed;
value.Selected = Visibility.Visible;
SetProperty(ref _Selected, value);
}
}
bool _ShowAnimations = true;
public bool ShowAnimations { get { return _ShowAnimations; } set { SetProperty(ref _ShowAnimations, value); } }
}
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void SetProperty<T>(ref T storage, T value, [System.Runtime.CompilerServices.CallerMemberName] String propertyName = null)
{
if (!object.Equals(storage, value))
{
storage = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
protected void RaisePropertyChanged([System.Runtime.CompilerServices.CallerMemberName] String propertyName = null)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Try this XAML:
<Page.DataContext>
<local:MyViewModel/>
</Page.DataContext>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="150" />
</Grid.RowDefinitions>
<FlipView ItemsSource="{Binding Colors}" SelectedItem="{Binding Selected, Mode=TwoWay}" UseTouchAnimationsForAllNavigation="{Binding ShowAnimations}">
<FlipView.ItemTemplate>
<DataTemplate>
<Rectangle>
<Rectangle.Fill>
<SolidColorBrush Color="{Binding Color}" />
</Rectangle.Fill>
</Rectangle>
</DataTemplate>
</FlipView.ItemTemplate>
</FlipView>
<ItemsControl ItemsSource="{Binding Colors}" Grid.Row="1" VerticalAlignment="Top" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<Grid>
<Rectangle Height="100" Width="100" Margin="10">
<Rectangle.Fill>
<SolidColorBrush Color="{Binding Color}" />
</Rectangle.Fill>
<Interactivity:Interaction.Behaviors>
<Core:EventTriggerBehavior EventName="PointerPressed">
<Core:CallMethodAction MethodName="SelectMe" TargetObject="{Binding}"/>
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Rectangle>
<TextBlock Text="X" VerticalAlignment="Top" HorizontalAlignment="Right" FontSize="50" Margin="20,10" Foreground="Wheat">
<Interactivity:Interaction.Behaviors>
<Core:EventTriggerBehavior EventName="PointerPressed">
<Core:CallMethodAction MethodName="RemoveMe" TargetObject="{Binding}"/>
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</TextBlock>
</Grid>
<Rectangle Height="10" Width="100" Margin="10"
Fill="White" Visibility="{Binding Selected}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Will look like this:
Best of luck!

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.

Categories

Resources