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

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">

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.

Display Items Using Grid in Xamarin Forms

I am working on Xamarin Forms(Android,IOS,Windows).
I trying to display items in a grid with selection (highlight the selected item).
Please find the below image for more information.
Can any one suggest me, How to achieve items display in grid with selection?
Generally each item binds a model which contains a boolean value that keeps selected or not. With that approach you ca initialize them as you want. Then create Gesture recognizer for tapping gesture & use Trigger for selection effect(BackgroundColor)
(In your definiton you mentioned to using inside Grid, so I'm not offering new methodology for UI hierarchy)
Here is theCode:
Model:
public Class ItemModel: INotifyPropertyChanged
{
// implement INotifyPropertyChanged interface
public ItemModel()
{
ToggleCommand = new Command(CmdToggle);
}
private void CmdToggle()
{
IsSelected = !IsSelected;
}
public string Name
{
get;
set; //call OnPropertyChanged
}
public bool IsSelected
{
get;
set; //call OnPropertyChanged
}
public ICommand ToggleCommand{get;private set;}
}
ViewModel
public Class PageViewModel: INotifyPropertyChanged
{
public List<ItemModel> Items
{
get;
set; //call OnPropertyChanged
}
}
Converter
public class BoolToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is bool)
return ((bool)value) ? Color.Gray: Color.White;
else
throw new NotSupportedException();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Xaml:
<Page.Resources>
<Color x:Key="SelectedColor">Gray</Color/>
<Color x:Key="UnselectedColor">White</Color/>
<namespace:BoolToColorConverter x:Key="BoolToColorConverter"/>
</Page.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="90"/>
<RowDefinition Height="90"/>
<RowDefinition Height="90"/>
<RowDefinition Height="90"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!--Single item -->
<StackLayout Grid.Row="0"
Grid.Column="0"
BindingContext="{Binding Items[0]}"
Orientation="Vertical"
BackgroundColor="{Binding IsSelected,Converter={StaticResource BoolToColorConverter}}"
>
<Image Source="{Binding yourImageProperty}" />
<Image Source="{Binding yourImage2Property}" />
<Label Source="{Binding Name}"/>
<StackLayout.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ToggleCommand}" />
</StackLayout.GestureRecognizers>
<!--Triggers will update background color when update IsSelected-->
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout" Binding="{Binding IsSelected}" Value="True">
<Setter Property="BackgroundColor" Value="{StaticResource SelectedColor}" />
</DataTrigger>
<DataTrigger TargetType="c:Label" Binding="{Binding IsSelected}" Value="False">
<Setter Property="BackgroundColor" Value="{StaticResource UnselectedColor}" />
</DataTrigger>
</StackLayout.Triggers>
</StackLayout>
</Grid>

WPF Binding not updating Visibility

I'm having trouble with the binding to a visibility of a grid. I've had projects where I've done this before and have tried to replicate the same coding used previously, and I've searched around and added in the bool to visibility converter based off some other articles but still nothing.
All that I am trying to do so far in this project is have a set of options that will be provided to the user. When they select one option it will either bring up sub-options or just take them to the proper area. I have the binding set to a bool object and have at times created little message boxes and others just to let me know if the program is reaching everywhere. I get every messagebox along the way, so it appears to be reaching every piece of code.
Can anyone shed some light on what I am doing wrong or point me in the correct direction?
Converter in Windows.Resources (Edited to show all code in Windows.Resources)
<Window.Resources>
<Style TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="15"/>
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Height" Value="50" />
<Setter Property="Width" Value="100" />
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
Code in the rest of the window
<Grid>
<Grid x:Name="grid_mainMenu" Visibility="{Binding MainMenuVisibility, Converter={StaticResource BooleanToVisibilityConverter}}" Margin="0,0,0,20">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Button x:Name="button_Items" Content="Items" Grid.Row="0" Click="button_Items_Click"/>
<Button x:Name="button_Orders" Content="Orders" Grid.Row="1" Click="button_Orders_Click" />
<TextBox Text="{Binding StatusMessage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Height="100" Width="100"/>
</Grid>
<Grid x:Name="grid_itemMenu" Visibility="{Binding ItemMenuVisibility, Converter={StaticResource BooleanToVisibilityConverter}}" Margin="0,0,0,20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Label Content="Item Menu" Grid.Row="0" FontSize="20" FontWeight="Bold" Margin="0,0,0,0" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<Button Grid.Row="1" x:Name="button_itemMaintenance" Content="Maintenance"/>
<Button Grid.Row="2" x:Name="button_itemCreation" Content="Create"/>
</Grid>
<DockPanel Height="25" Margin="0,0,0,0" VerticalAlignment="Bottom">
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem>
<TextBlock Text="{Binding StatusMessage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StatusBarItem>
</StatusBar>
</DockPanel>
</Grid>
Here is the code in the class
public bool MainMenuVisibility
{
get { return _mainMenuVisibility; }
set { _mainMenuVisibility = value; RaisePropertyChanged(); }
}
public bool ItemMenuVisibility
{
get { return _itemMenuVisibility; }
set
{ _itemMenuVisibility = value; RaisePropertyChanged(); }
}
public bool OrderMenuVisibility
{
get { return _orderMenuVisibility; }
set { _orderMenuVisibility = value; RaisePropertyChanged(); }
}
Main Constructor
public Menu_View()
{
ShowMainMenu();
}
A couple of the controls
public void ShowMainMenu()
{
MainMenuVisibility = true;
HideItemMenu();
HideOrderMenu();
StatusMessage = "Showing main menu";
}
public void HideMainMenu()
{
MainMenuVisibility = false;
StatusMessage = "Hid main menu";
}
public void ShowItemMenu()
{
try
{
//Reaches, but never updates
ItemMenuVisibility = true;
HideMainMenu();
HideOrderMenu();
}
catch(Exception error)
{
//Never shows anything here
StatusMessage = "Failed to load item menu";
}
finally
{
//Does not update, but reaches here
StatusMessage = "Showing item menu";
}
}
Program starts by showing the main menu, when user clicks a button for Items it is supposed to show the item menu. The button click calls ShowItemMenu(). I have verified that that does happen and is called in proper order.
I have verified that ShowItemMenu() does work but putting in the constructor instead of ShowMainMenu(). Either one works fine, but neither will cause an update after the initial loading even though they are reached after button presses.
Sorry if I did not include everything I needed.
EDIT:
I believe that I actually had two issues going on simultaneously. One an improperly configured data converter. Answer and reference below.
As well as an issue in my window code here:
public MainWindow()
{
InitializeComponent();
menuView = new Menu_View();
this.DataContext = new Menu_View();
}
Menu_View menuView;
I believe this was part of the issue. I was creating a menuView of type Menu_View. On initialize I assigned menuView to a new Menu_View() and then assigned my DataContext to a new Menu_View() instead of menuView. All commands were updating menuView and I was updating the one assigned to the DataContext.
Add this converter class to your project.
class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is Boolean && (bool)value)
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is Visibility && (Visibility)value == Visibility.Visible)
{
return true;
}
return false;
}
}
Referance
You could go without the Boolean converter if you choose. You won't have to write any code-behind.
<Grid>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MyBoolValue}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>

Change style based on data

Consider below XAML =>
<fluent:Ribbon x:Name="MenuRibbon"
Title="title"
SnapsToDevicePixels="True">
<fluent:RibbonTabItem x:Name="Home"
Header="Home">
<fluent:RibbonGroupBox Header="Project">
<fluent:InRibbonGallery MinItemsInRow="3"
MaxItemsInRow="6"
Width="300"
ItemWidth="64"
ItemHeight="56"
ItemsSource="{Binding Projects}">
<fluent:InRibbonGallery.ItemTemplate>
<DataTemplate>
<DockPanel>
<Border BorderBrush="{x:Null}" Height="56">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" DockPanel.Dock="Bottom"
Text="{Binding Name}">
</TextBlock>
</Border>
</DockPanel>
</DataTemplate>
</fluent:InRibbonGallery.ItemTemplate>
</fluent:InRibbonGallery>
</fluent:RibbonGroupBox>
</fluent:RibbonTabItem>
</fluent:Ribbon>
I've bind ObservableColleciton of Projects to InRibbonGallery , And there is an instance of project (ActiveProject) exist in ViewModel.
a TextBlock defined in DataTemplate to display Name of Project object.
How can I change color of TextBlock that contains Active Project ?
ViewModel :
public class ViewModel : ViewModelBase
{
private ObservableCollection<Project> _projects;
public ViewModel()
{
Projects = new ObservableCollection<Project>(new List<Project>
{
new Project {Id = "0", Name = "Project1"},
new Project {Id = "1", Name = "Project2"},
new Project {Id = "2", Name = "Project3"}
});
Project = Projects[0];
}
public ObservableCollection<Project> Projects
{
get { return _projects; }
set
{
_projects = value;
RaisePropertyChanged(() => Projects);
}
}
public Project Project { get; set; }
}
public class Project : ObservableObject
{
private string _id;
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
RaisePropertyChanged(() => Name);
}
}
public string Id
{
get { return _id; }
set
{
_id = value;
RaisePropertyChanged(() => Id);
}
}
}
Project files located here .
You can do this by using a MultiBinding and a DataTrigger in combination for a better result.
so your xaml would look like:
<Window.Resources>
<vm:ViewModel x:Key="ViewModel" />
<vm:ActiveProjectCheckConverter x:Key="ActiveProjectCheckConverter" />
</Window.Resources>
...
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
DockPanel.Dock="Bottom"
Text="{Binding Name}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Background"
Value="Transparent" />
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource ActiveProjectCheckConverter}">
<Binding Path="Name" />
<Binding Path="DataContext.ActiveProject.Name"
RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type fluent:InRibbonGallery}}" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="1,0">
<GradientStop Offset="0.0"
Color="#00FFFFFF" />
<GradientStop Offset="1.1"
Color="#FFFFFFFF" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
and your converter:
public class ActiveProjectCheckConverter : IMultiValueConverter {
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
string first = values[0] as string;
string second = values[1] as string;
return !string.IsNullOrEmpty(first) && !string.IsNullOrEmpty(second) && first == second;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
Now I did make one more change, your Project object in your ViewModel, if you want changes to that to reflect in the View you need to make that implement INPC itself. So I did update that and also renamed it to ActiveProject
private Project _activeProject;
public Project ActiveProject {
get {
return _activeProject;
}
set {
if (value == _activeProject)
return;
_activeProject = value;
RaisePropertyChanged(() => ActiveProject);
}
}
Update
You can find the above updates at: Dropbox-Link

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