Position a button after the last listviewitem - c#

My ListView is bound to an ObservableCollection, is there a way to position a button after the last listviewitem? What I have done is define the button in the DataTemplate like below:
<DataTemplate x:Key="TestDataTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="SeletedFilterText" Text="{Binding}" />
<Button Command="{Binding DataContext.TestCommand,ElementName=TestListView}"
Content="Test"
Visibility="{Binding Converter={StaticResource testConverter}}"
Grid.Column="1"/>
</Grid>
</DataTemplate>
In my ViewModel, I define a string variable to store the last item. The ItemSource(an Observable) may add or remove item, every time I set the last of the Collection to the LastItem variable. In the converter, compare the binding content with the LastItem, if the value is true, display the Button, if false, hide it. But the converter will never be triggered. Anyone can help?

I would suggest not to have backup field in ViewModel to keep track of lastItem in collection.
You can do that with only Converter in place which will return true or false if passed ListViewItem is last item in ListView or not.
In case you want to call the converter whenever underlying ObservableCollection add/remove item in it, I would suggest to pass Count property to converter so that converter gets fired whenever item is added/removed from collection.
Converter code:
public class IsLastItemInContainerConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
CultureInfo culture)
{
DependencyObject item = (DependencyObject)values[0];
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(item);
if (ic != null)
{
return ic.ItemContainerGenerator.IndexFromContainer(item)
== ic.Items.Count - 1;
}
else
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML:
<Button Content="Test">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding
Converter="{StaticResource IsLastItemInContainerConverter}">
<Binding Path="."
RelativeSource="{RelativeSource Mode=FindAncestor,
AncestorType=ListViewItem}"/>
<Binding Path="DataContext.SourceCollection.Count"
RelativeSource="{RelativeSource Mode=FindAncestor,
AncestorType=ListView}"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
Replace SourceCollection with your ObservableCollection name in dataTrigger.

I would suggest you create a custom control for your use case. Like so:
public class ButtonListView : ListView
{
static ButtonListView()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ButtonListView), new FrameworkPropertyMetadata(typeof(ButtonListView)));
}
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
"Command", typeof (ICommand), typeof (ButtonListView), new PropertyMetadata(default(ICommand)));
public ICommand Command
{
get { return (ICommand) GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public static readonly DependencyProperty ButtonContentProperty = DependencyProperty.Register(
"ButtonContent", typeof (object), typeof (ButtonListView), new PropertyMetadata(default(object)));
public object ButtonContent
{
get { return (object) GetValue(ButtonContentProperty); }
set { SetValue(ButtonContentProperty, value); }
}
}
And use this style:
<SolidColorBrush x:Key="ListBox.Disabled.Background" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ListBox.Disabled.Border" Color="#FFD9D9D9" />
<Style TargetType="{x:Type local:ButtonListView}" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ButtonListView}">
<Border Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="true"
Padding="1">
<ScrollViewer Padding="{TemplateBinding Padding}"
Focusable="false">
<StackPanel>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
<Button Content="{TemplateBinding ButtonContent}" Command="{TemplateBinding Command}"></Button>
</StackPanel>
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource ListBox.Disabled.Background}" />
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource ListBox.Disabled.Border}" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsGrouping" Value="true" />
<Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false" />
</MultiTrigger.Conditions>
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You can then use it like so:
<wpfSandbox:ButtonListView ButtonContent="Press" Command="{Binding ...}"/>
This way you don't need to Keep track of the order in the ObservableCollection

Yes and it's very easy:
Define your Observable collection with generic type of <DependencyObject>
Add a custom object to the end of the collection. (it can be a something like a ViewModel for Button if you want to add commands or etc to it)
Don't set the ItemTemplate of your ListView (or ItemsControl or etc)
Instead, define two DataTemplates without x:Key in the resources and set their DataType to the desired types. It should be like "{x:Type local:ButtonVm}" or "{x:Type vm:ListViewItemType}"
Now the template for each item automatically set to the data template that matches the type of that item.
Example:
(note that you can move ListView.Resources to Window.Resources if the templates can be reused elsewhere)
MainWindow.xaml:
<ListView ItemsSource="{Binding Items}">
<ListView.Resources>
<DataTemplate DataType="{x:Type vm:ListItemVm}">
<TextBlock Text="{Binding ItemText}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ButtonVm}">
<Button Command="{Binding ButtonCommand}">
<TextBlock Text="{Binding ButtonText}"/>
</Button>
</DataTemplate>
</ListView.Resources>
</ListView>
MainWindow.xaml.cs:
public MainWindow()
{
InitializeComponent();
DataContext = this;
Items.Add(new ListItemVm { ItemText = "something" } );
Items.Add(new ListItemVm { ItemText = "something" } );
Items.Add(new ListItemVm { ItemText = "something" } );
Items.Add(new ButtonVm { ButtonText = "click here" } );
}
private ObservableCollection<DependencyObject> _items = new ObservableCollection<DependencyObject>();
public ObservableCollection<DependencyObject> Items { get { return _items; } }
one viewModel for each type of item:
public class ListItemVm : DependencyObject
{
public string ItemText
{
get { return (string)GetValue(ItemTextProperty); }
set { SetValue(ItemTextProperty, value); }
}
public static readonly DependencyProperty ItemTextProperty =
DependencyProperty.Register("ItemText", typeof(string), typeof(ListItemVm), new UIPropertyMetadata(""));
}
public class ButtonVm : DependencyObject
{
public string ButtonText
{
get { return (string)GetValue(ButtonTextProperty); }
set { SetValue(ButtonTextProperty, value); }
}
public static readonly DependencyProperty ButtonTextProperty =
DependencyProperty.Register("ButtonText", typeof(string), typeof(ButtonVm), new UIPropertyMetadata(""));
public Command ButtonCommand
{
get { return (string)GetValue(ButtonCommandProperty); }
set { SetValue(ButtonCommandProperty, value); }
}
public static readonly DependencyProperty ButtonCommandProperty =
DependencyProperty.Register("ButtonCommand", typeof(Command), typeof(ButtonVm), new UIPropertyMetadata(""));
}
public class Command : ICommand { /* simple implementation of ICommand */ }

Related

Binding the style of the control to a Viewmodel property

I am trying to create a banner with different states and colors to show messages to the user, showing a Border with a specific style and a Label with an style bases in the Border style and using DataTrigger. I have created each custom styles for each state in my App.xaml and I am trying to change the state based on a property of my ViewModel.
The problem is that the style doesn't change every time I change the property, but nevertheless if I modify some of the XAML during debugging, the style is refreshed correctly.
Maybe I am missing a NotifyPropertyChanged somewhere?
Visual example:
This is My code:
App.xaml with my defined styles and my custom converter
<Application.Resources>
<local:StyleConverter x:Key="StyleConverter" />
<Style x:Key="Banner" TargetType="{x:Type Border}">
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="10" />
<Setter Property="CornerRadius" Value="5" />
</Style>
<Style x:Key="Banner_Info" TargetType="{x:Type Border}" BasedOn="{StaticResource Banner}">
<Setter Property="Background" Value="#e3f7fc" />
<Setter Property="BorderBrush" Value="#8ed9f6" />
</Style>
<Style x:Key="Banner_Error" TargetType="{x:Type Border}" BasedOn="{StaticResource Banner}">
<Setter Property="Background" Value="#ffecec" />
<Setter Property="BorderBrush" Value="#f5aca6" />
</Style>
<Style x:Key="Banner_Success" TargetType="{x:Type Border}" BasedOn="{StaticResource Banner}">
<Setter Property="Background" Value="#e9ffd9" />
<Setter Property="BorderBrush" Value="#a6ca8a" />
</Style>
<Style x:Key="Banner_Text" TargetType="{x:Type Label}">
<Setter Property="FontWeight" Value="DemiBold"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding BorderBrush.Color, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Border}}}" Value="#8ed9f6">
<Setter Property="Foreground" Value="#31708F" />
</DataTrigger>
<DataTrigger Binding="{Binding BorderBrush.Color, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Border}}}" Value="#f5aca6">
<Setter Property="Foreground" Value="#B10009" />
</DataTrigger>
<DataTrigger Binding="{Binding BorderBrush.Color, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Border}}}" Value="#a6ca8a">
<Setter Property="Foreground" Value="#529214" />
</DataTrigger>
</Style.Triggers>
</Style>
</Application.Resources>
Styleconverter.cs
public class StyleConverter : IValueConverter {
public object Convert (object value, Type targetType, object parameter, CultureInfo culture) {
if (targetType != typeof (Style)) {
throw new InvalidOperationException ("The target must be a Style");
}
var styleProperty = parameter as string;
if (value == null || styleProperty == null) {
return null;
}
string styleValue = value.GetType ()
.GetProperty (styleProperty)
.GetValue (value, null)
.ToString ();
if (styleValue == null) {
return null;
}
Style newStyle = (Style) Application.Current.TryFindResource (styleValue);
return newStyle;
}
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException ();
}
}
Banner.cs
public class Banner : INotifyPropertyChanged {
private string _Message;
public string Message {
get => _Message;
private set {
_Message = value;
RaisePropertyChanged (null);
}
}
private string _Style = "Banner_Success";
public string Style {
get => _Style;
private set {
_Style = value;
RaisePropertyChanged (null);
}
}
public Banner SetSuccess (string message) {
Style = "Banner_Success";
Message = message;
return this;
}
public Banner SetInfo (string message) {
Style = "Banner_Info";
Message = message;
return this;
}
public Banner SetError (string message) {
Style = "Banner_Error";
Message = message;
return this;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged (string PropertyName) {
PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (PropertyName));
}
#endregion
}
Mainwindow.xml
<Grid>
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"></ColumnDefinition>
<ColumnDefinition Width="10*"></ColumnDefinition>
<ColumnDefinition Width="2*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Border Grid.Column="1" Grid.Row="0" VerticalAlignment="Center" Style="{Binding ., Mode=TwoWay, Converter={StaticResource StyleConverter}, ConverterParameter=BannerStyle}">
<Label Grid.Column="1" Style="{StaticResource Banner_Text}" Content="{Binding BannerMessage}" />
</Border>
</Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Padding="10" Margin="10" Content="BLUE" Command="{Binding CmdChangeColor, UpdateSourceTrigger=PropertyChanged}" CommandParameter="blue" />
<Button Padding="10" Margin="10" Content="RED" Command="{Binding CmdChangeColor, UpdateSourceTrigger=PropertyChanged}" CommandParameter="red" />
<Button Padding="10" Margin="10" Content="GREEN" Command="{Binding CmdChangeColor, UpdateSourceTrigger=PropertyChanged}" CommandParameter="green" />
</StackPanel>
</StackPanel>
</Grid>
MainViewModel.cs
public class MainViewModel : INotifyPropertyChanged {
public Banner Banner { get; } = new Banner ();
public string BannerStyle => Banner.Style;
public string BannerMessage => Banner.Message;
public RelayCommand CmdChangeColor { get; }
public MainViewModel () {
Banner.SetSuccess ("This is the initial message");
CmdChangeColor = new RelayCommand (param => ChangeColor (param.ToString ()));
}
public void ChangeColor (string color) {
switch (color) {
case "blue":
Banner.SetInfo ("INFO!! This should be a banner with blue background");
break;
case "red":
Banner.SetError ("ERROR!! This should be a banner with red background");
break;
case "green":
Banner.SetSuccess ("SUCCESS!! This should be a banner with green background");
break;
}
RaisePropertyChanged (null);
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged (string PropertyName) {
PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (PropertyName));
}
#endregion
}
bind to property which has notifications, instead of entire view model ({Binding .}). This way also simplifies converter (no more reflection)
<Border Style="{Binding Path=Banner.Style, Converter={StaticResource StyleConverter}">
public class StyleConverter : IValueConverter {
public object Convert (object value, Type targetType, object parameter, CultureInfo culture) {
if (targetType != typeof (Style)) {
throw new InvalidOperationException ("The target must be a Style");
}
string styleValue = value?.ToString();
if (styleValue == null) {
return null;
}
Style newStyle = (Style) Application.Current.TryFindResource (styleValue);
return newStyle;
}
public object ConvertBack (object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException ();
}
}

Wpf Custom Control: How to implement CustomContent

I'm working on a CustomControl in WPF. I want to Implement a CardControl. I have two Contents "Front" and "Back" and I want to flip between them.
But I can't define a Working Trigger in the Style to flip between the contents...
First I created a Custom Control, with DependencyProperties:
public class Card : Control {
#region initializer
static Card() {
DefaultStyleKeyProperty.OverrideMetadata(typeof(Card), new FrameworkPropertyMetadata(typeof(Card)));
}
#endregion
#region dependencyProperties
public FrameworkElement CustomContent {
get { return (FrameworkElement)GetValue(CustomContentProperty); }
set { SetValue(CustomContentProperty, value); }
}
public static DependencyProperty CustomContentProperty =
DependencyProperty.Register(nameof(CustomContent), typeof(FrameworkElement), typeof(Card), new PropertyMetadata());
public FrameworkElement Front {
get { return (FrameworkElement)GetValue(FrontProperty); }
set { SetValue(FrontProperty, value); }
}
public static DependencyProperty FrontProperty =
DependencyProperty.Register(nameof(Front), typeof(FrameworkElement), typeof(Card), new PropertyMetadata());
public FrameworkElement Back {
get { return (FrameworkElement)GetValue(BackProperty); }
set { SetValue(BackProperty, value); }
}
public static DependencyProperty BackProperty =
DependencyProperty.Register(nameof(Back), typeof(FrameworkElement), typeof(Card), new PropertyMetadata());
public bool IsDetailed {
get { return (bool)GetValue(IsDetailedProperty); }
set { SetValue(IsDetailedProperty, value); }
}
public static readonly DependencyProperty IsDetailedProperty =
DependencyProperty.Register(nameof(IsDetailed), typeof(bool), typeof(Card), new PropertyMetadata(false));
public CornerRadius CornerRadius {
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(Card));
#endregion
Then I defined the Style in Generic.xaml:
<Style TargetType="{x:Type local:Card}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Card}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
Content="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:Card}}, Path=CustomContent,
Converter={StaticResource Debugger}, UpdateSourceTrigger=PropertyChanged}"/>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource
AncestorType={x:Type local:Card}},
Path=isDetailed,
Converter={StaticResource Debugger}}"
Value="True">
<Setter Property="CustomContent"
Value="{Binding RelativeSource={RelativeSource
AncestorType={x:Type local:Card}},
Path=Back}"/>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource
AncestorType={x:Type local:Card}},
Path=isDetailed}"
Value="False">
<Setter Property="CustomContent"
Value="{Binding RelativeSource={RelativeSource
AncestorType={x:Type local:Card}},
Path=Front}"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And I Implemented a Simple CardControl for Test-Purposes with a Button, which flips the isDetailed state:
<StackPanel>
<CustomCharts:Card Name="TestCard">
<CustomCharts:Card.CustomContent>
<TextBlock Text="Hello World!"/>
</CustomCharts:Card.CustomContent>
<CustomCharts:Card.Front>
<TextBlock Text="Wow vordere sache!"/>
</CustomCharts:Card.Front>
<CustomCharts:Card.Back>
<TextBlock Text="Wow hintere sache!"/>
</CustomCharts:Card.Back>
</CustomCharts:Card>
<Button Click="Button_Click" Width="50" Height="20" Content="Test"/>
</StackPanel>
private void Button_Click( object sender, RoutedEventArgs e ) {
this.TestCard.IsDetailed= !this.TestCard.IsDetailed;
}
Any Help is apreciated, I'm stuck...
you can benefit a lot if declare Front and Back with object type, not FrameworkElement - mostly because it will allow easy bidnings. you can keep rich visual appearance if you add corresponding templates for those two properties.
public class Card : Control
{
static Card()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Card), new FrameworkPropertyMetadata(typeof(Card)));
}
public object Front
{
get { return (object)GetValue(FrontProperty); }
set { SetValue(FrontProperty, value); }
}
public static readonly DependencyProperty FrontProperty =
DependencyProperty.Register("Front", typeof(object), typeof(Card), new PropertyMetadata(null));
public DataTemplate FrontTemplate
{
get { return (DataTemplate)GetValue(FrontTemplateProperty); }
set { SetValue(FrontTemplateProperty, value); }
}
public static readonly DependencyProperty FrontTemplateProperty =
DependencyProperty.Register("FrontTemplate", typeof(DataTemplate), typeof(Card), new PropertyMetadata(null));
public object Back
{
get { return (object)GetValue(BackProperty); }
set { SetValue(BackProperty, value); }
}
public static readonly DependencyProperty BackProperty =
DependencyProperty.Register("Back", typeof(object), typeof(Card), new PropertyMetadata(null));
public DataTemplate BackTemplate
{
get { return (DataTemplate)GetValue(BackTemplateProperty); }
set { SetValue(BackTemplateProperty, value); }
}
public static readonly DependencyProperty BackTemplateProperty =
DependencyProperty.Register("BackTemplate", typeof(DataTemplate), typeof(Card), new PropertyMetadata(null));
public bool IsDetailed
{
get { return (bool)GetValue(IsDetailedProperty); }
set { SetValue(IsDetailedProperty, value); }
}
public static readonly DependencyProperty IsDetailedProperty =
DependencyProperty.Register(nameof(IsDetailed), typeof(bool), typeof(Card), new PropertyMetadata(false));
public CornerRadius CornerRadius
{
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(Card), new PropertyMetadata(new CornerRadius(0)));
}
Use Trigger on IsDetailed property to flip Fron and Back:
<Style TargetType="{x:Type local:Card}">
<Setter Property="Background" Value="Khaki"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Card}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter x:Name="presenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsDetailed" Value="True">
<Setter TargetName="presenter" Property="Content"
Value="{Binding Front, RelativeSource={RelativeSource TemplatedParent}}"/>
<Setter TargetName="presenter" Property="ContentTemplate"
Value="{Binding FrontTemplate, RelativeSource={RelativeSource TemplatedParent}}"/>
</Trigger>
<Trigger Property="IsDetailed" Value="False">
<Setter TargetName="presenter" Property="Content"
Value="{Binding Back, RelativeSource={RelativeSource TemplatedParent}}"/>
<Setter TargetName="presenter" Property="ContentTemplate"
Value="{Binding BackTemplate, RelativeSource={RelativeSource TemplatedParent}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
use TemplateBindings inside Template, and RelativeSource TemplatedParent with normal Binding in Setters (TemplateBinding is not supported in Setter).
And here is two examples of usage - with and without binding:
<StackPanel DataContext="{x:Static sys:DateTime.Now}">
<CheckBox Name="chk_1"/>
<local:Card Front="Front"
Back="Back"
IsDetailed="{Binding IsChecked, ElementName=chk_1}"/>
<CheckBox Name="chk_2"/>
<local:Card Front="{Binding DayOfWeek}"
Back="{Binding TimeOfDay}"
IsDetailed="{Binding IsChecked, ElementName=chk_2}">
<local:Card.FrontTemplate>
<DataTemplate>
<TextBlock Foreground="Red" FontWeight="Bold" FontSize="20"
Text="{Binding}" TextDecorations="Underline"/>
</DataTemplate>
</local:Card.FrontTemplate>
<local:Card.BackTemplate>
<DataTemplate>
<TextBlock Foreground="Blue" FontWeight="Bold" FontSize="20"
Text="{Binding}" TextDecorations="Underline"/>
</DataTemplate>
</local:Card.BackTemplate>
</local:Card>
</StackPanel>
result:

Check the entered value in textboxes are double number in WPF

I am very new to WPF world so do not have much ideas about doing it. Basically,
I want to check whether value entered into textboxes are double number or not.If the value is double number then change the result textbox value to NAN and also change the color of input textbox to red.
Can anyone please guide me how to accomplish this?
Model.cs
public abstract class ObservableBase : INotifyPropertyChanged
{
public void Set<TValue>(ref TValue field, TValue newValue, [CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<TValue>.Default.Equals(field, default(TValue)) && field.Equals(newValue)) return;
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public abstract class ViewModelBase : ObservableBase
{
public bool IsInDesignMode
=> (bool)DesignerProperties.IsInDesignModeProperty
.GetMetadata(typeof(DependencyObject))
.DefaultValue;
}
ViewModel.cs
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
if (IsInDesignMode)
{
valueA = 2;
valueB = 3;
Calc();
}
}
#region Properties
private int valueA;
public int ValueA
{
get => valueA;
set
{
Set(ref valueA, value);
Calc();
}
}
private int valueB;
public int ValueB
{
get => valueB;
set
{
Set(ref valueB, value);
Calc();
}
}
private int valueC;
public int ValueC
{
get => valueC;
set => Set(ref valueC, value);
}
private int valueD;
public int ValueD
{
get => valueD;
set => Set(ref valueD, value);
}
#endregion
#region Methods
private void Calc()
{
ValueC = valueA + valueB;
ValueD = valueA * valueB;
}
#endregion
}
XAML
<Window x:Class="WPFTestApplication.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:WPFTestApplication.ViewModel"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="TextBox" x:Key="TextBox">
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Margin" Value="10"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
<Setter Property="Grid.Column" Value="1"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Silver" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox" x:Key="TextBoxA" BasedOn="{StaticResource TextBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox" x:Key="TextBoxB" BasedOn="{StaticResource TextBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightGreen" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="TextBox" BasedOn="{StaticResource TextBox}"/>
</Grid.Resources>
<TextBlock Text="Value A"/>
<TextBox Text="{Binding ValueA, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBoxA}"/>
<TextBlock Text="Value B" Grid.Row="1"/>
<TextBox Text="{Binding ValueB, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBoxB}"
Grid.Row="1"/>
<TextBlock Text="Value C" Grid.Row="2"/>
<TextBox Text="{Binding ValueC}"
IsReadOnly="True"
Grid.Row="2"/>
<TextBlock Text="Value D" Grid.Row="3"/>
<TextBox Text="{Binding ValueD}"
IsReadOnly="True"
Grid.Row="3"/>
</Grid>
</Window>
The easiest way for your problem is to implement a ValidationRule.
Here is the code for the rule:
public class DoubleValidation : ValidationRule
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
//You can do whatever you want here
double check;
if (!double.TryParse(value.ToString(),out check))
{
//ValidationResult(false,*) => in error
return new ValidationResult(false, "Please enter a number");
}
//ValidationResult(true,*) => is ok
return new ValidationResult(true, null);
}
}
Then in your XAML you have to refer to this ValidationRule when binding, which allows you to get the Validation.HasError property in your style.
<TextBox Validation.ErrorTemplate="{x:Null}">
<TextBox.Text>
<Binding Path="ValueB" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DoubleValidation/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<TextBox.Style>
<Style BasedOn="{StaticResource TextBoxB}" TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
Since the Error will add a red border to the TextBox, I add Validation.ErrorTemplate="{x:Null}" to keep full control.
If you want to change the Textbox value to NaN, you should do it in your ViewModel. But I would disrecommend that, since it's very boring for the user to see its inputs getting change by the UI.
I have modified your code to achieve your goal:
Model.cs
I have added ObservableBase.NotifyPropertyChanged()
public abstract class ObservableBase : INotifyPropertyChanged
{
public void Set<TValue>(ref TValue field, TValue newValue, [CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<TValue>.Default.Equals(field, default(TValue)) && field.Equals(newValue)) return;
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public abstract class ViewModelBase : ObservableBase
{
public bool IsInDesignMode
=> (bool)DesignerProperties.IsInDesignModeProperty
.GetMetadata(typeof(DependencyObject))
.DefaultValue;
}
Then your ViewModel would look like this, you see I changed the types from int to string, then added validation flags, the trick to check whether the input is double or not is to use double.TryParse.
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
valueAisValid = true;
valueBisValid = true;
if (IsInDesignMode)
{
Calc();
}
}
#region Properties
private string valueA;
public string ValueA
{
get => valueA;
set
{
if (!string.IsNullOrEmpty(value))
{
Set(ref valueA, value);
Set(ref valueAisValid, double.TryParse(ValueA, out double d));
NotifyPropertyChanged(nameof(ValueAIsValid));
Calc();
}
}
}
private bool valueAisValid;
public bool ValueAIsValid => valueAisValid;
private string valueB;
public string ValueB
{
get => valueB;
set
{
if (!string.IsNullOrEmpty(value))
{
Set(ref valueB, value);
Set(ref valueBisValid, double.TryParse(ValueB, out double d));
NotifyPropertyChanged(nameof(ValueBIsValid));
Calc();
}
}
}
private bool valueBisValid;
public bool ValueBIsValid => valueBisValid;
private string valueC;
public string ValueC
{
get => valueC;
set => Set(ref valueC, value);
}
private string valueD;
public string ValueD
{
get => valueD;
set => Set(ref valueD, value);
}
public bool InputsValid => ValueAIsValid && ValueBIsValid;
#endregion
#region Methods
private void Calc()
{
if (InputsValid)
{
double sum = Convert.ToDouble(valueA) + Convert.ToDouble(valueB);
double product = Convert.ToDouble(valueA) * Convert.ToDouble(valueB);
ValueC = sum.ToString(CultureInfo.InvariantCulture);
ValueD = product.ToString(CultureInfo.InvariantCulture);
}
else
{
ValueC = "NAN";
ValueD = "NAN";
}
}
#endregion
}
Now here is the new guy, meet BoolToBackgroundColorConverter.
namespace WPFTestApplication
{
public class BoolToBackgroundColorConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null && !(bool)value)
{
return new SolidColorBrush(Colors.Red);
}
else if(value != null && (bool)value && parameter != null)
{
return (SolidColorBrush)parameter;
}
else
{
return new SolidColorBrush(Colors.White);
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Now your xaml would look like:
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<local:BoolToBackgroundColorConverter x:Key="BoolToBackgroundColorConverter"/>
</Window.Resources>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<SolidColorBrush x:Key="LightGreen" Color="LightGreen" />
<SolidColorBrush x:Key="LightBlue" Color="LightBlue" />
<SolidColorBrush x:Key="White" Color="White" />
<Style TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style TargetType="TextBox" x:Key="TextBox">
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Margin" Value="10"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="25"/>
<Setter Property="Grid.Column" Value="1"/>
</Style>
<Style TargetType="TextBox" x:Key="TextBoxA" BasedOn="{StaticResource TextBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{Binding ValueAIsValid, Converter={StaticResource BoolToBackgroundColorConverter}, ConverterParameter={StaticResource LightBlue}}" />
</Trigger>
</Style.Triggers>
<Setter Property="Background" Value="{Binding ValueAIsValid, Converter={StaticResource BoolToBackgroundColorConverter}, ConverterParameter={StaticResource White}}" />
</Style>
<Style TargetType="TextBox" x:Key="TextBoxB" BasedOn="{StaticResource TextBox}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{Binding ValueBIsValid, Converter={StaticResource BoolToBackgroundColorConverter}, ConverterParameter={StaticResource LightGreen}}" />
</Trigger>
</Style.Triggers>
<Setter Property="Background" Value="{Binding ValueBIsValid, Converter={StaticResource BoolToBackgroundColorConverter}, ConverterParameter={StaticResource White}}" />
</Style>
<Style TargetType="TextBox" BasedOn="{StaticResource TextBox}"/>
</Grid.Resources>
<TextBlock Text="Value A"/>
<TextBox Text="{Binding ValueA, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBoxA}"/>
<TextBlock Text="Value B" Grid.Row="1"/>
<TextBox Text="{Binding ValueB, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource TextBoxB}"
Grid.Row="1"/>
<TextBlock Text="Value C" Grid.Row="2"/>
<TextBox Text="{Binding ValueC}"
IsReadOnly="True"
Grid.Row="2"/>
<TextBlock Text="Value D" Grid.Row="3"/>
<TextBox Text="{Binding ValueD}"
IsReadOnly="True"
Grid.Row="3"/>
</Grid>
Output:
Hope this helps!
The easiest way to do this is to handle the validation in the View layer - using a control such as DoubleUpDown from the Extended WPF Toolkit, rather than trying to validate in the ViewModel by parsing a text string.

Proper DataGrid search from TextBox in WPF using MVVM

I am new to the MVVM pattern, and a little confused on when to use Code Behind. I have a very simple form right now, that includes one TextBox, and one DataGrid. What I would like is to be able to have the DataGrid change its selected item based on the TextBox.
I have done this in Code Behind and it works fine using the following code:
private void textBox1_TextChanged(object sender, TextChangedEventArgs e)
{
for (int i = 0; i < dataGrid1.Items.Count; i++)
{
string cellContent = dtReferral.Rows[i][0].ToString();
try
{
if (cellContent != null && cellContent.Substring(0, textBox1.Text.Length).Equals(textBox1.Text))
{
object item = dataGrid1.Items[i];
dataGrid1.SelectedItem = item;
dataGrid1.ScrollIntoView(item);
//row.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
break;
}
}
catch { }
}
}
Now, I just want to highlight the Item in the Datagrid that starts with text in textbox, and allow the user to press a button to edit selected item.
Is it okay to have this logic in the Code Behind file? Or would I need to do this through some sort of binding? If I should do this through the View Model with Binding, any direction would be appreciated. Thank you.
If you only want to highlight the cells with the text from the TextBox you could make an AttatchedProperty for the DataGrid to accept your search value from the TextBox and create another AttatchedProperty for the Cell to indicate a match that you can usee to set properties in the Cell style. Then we create a IMultiValueConverter to check the Cell value for a match to the search Text.
This way its reusable on other projects as you only need the AttachedProperties and Converter
Bind the AttachedProperty SearchValue to your TextBox Text property.
<DataGrid local:DataGridTextSearch.SearchValue="{Binding ElementName=SearchBox, Path=Text, UpdateSourceTrigger=PropertyChanged}"
Then create a Style for DataGridCell and create a Setter for the AttachedProperty IsTextMatch using the IMultiValueConverter to return if the cells text matches the SearchValue
<Setter Property="local:DataGridTextSearch.IsTextMatch">
<Setter.Value>
<MultiBinding Converter="{StaticResource SearchValueConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="Content.Text" />
<Binding RelativeSource="{RelativeSource Self}" Path="(local:DataGridTextSearch.SearchValue)" />
</MultiBinding>
</Setter.Value>
</Setter>
Then we can use the Cells attached IsTextMatch property to set a highlight using a Trigger
<Style.Triggers>
<Trigger Property="local:DataGridTextSearch.IsTextMatch" Value="True">
<Setter Property="Background" Value="Orange" />
</Trigger>
</Style.Triggers>
Here is a working example showing my rambilings :)
Code:
namespace WpfApplication17
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
for (int i = 0; i < 20; i++)
{
TestData.Add(new TestClass { MyProperty = GetRandomText(), MyProperty2 = GetRandomText(), MyProperty3 = GetRandomText() });
}
}
private string GetRandomText()
{
return System.IO.Path.GetFileNameWithoutExtension(System.IO.Path.GetRandomFileName());
}
private ObservableCollection<TestClass> _testData = new ObservableCollection<TestClass>();
public ObservableCollection<TestClass> TestData
{
get { return _testData; }
set { _testData = value; }
}
}
public class TestClass
{
public string MyProperty { get; set; }
public string MyProperty2 { get; set; }
public string MyProperty3 { get; set; }
}
public static class DataGridTextSearch
{
// Using a DependencyProperty as the backing store for SearchValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty SearchValueProperty =
DependencyProperty.RegisterAttached("SearchValue", typeof(string), typeof(DataGridTextSearch),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.Inherits));
public static string GetSearchValue(DependencyObject obj)
{
return (string)obj.GetValue(SearchValueProperty);
}
public static void SetSearchValue(DependencyObject obj, string value)
{
obj.SetValue(SearchValueProperty, value);
}
// Using a DependencyProperty as the backing store for IsTextMatch. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsTextMatchProperty =
DependencyProperty.RegisterAttached("IsTextMatch", typeof(bool), typeof(DataGridTextSearch), new UIPropertyMetadata(false));
public static bool GetIsTextMatch(DependencyObject obj)
{
return (bool)obj.GetValue(IsTextMatchProperty);
}
public static void SetIsTextMatch(DependencyObject obj, bool value)
{
obj.SetValue(IsTextMatchProperty, value);
}
}
public class SearchValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
string cellText = values[0] == null ? string.Empty : values[0].ToString();
string searchText = values[1] as string;
if (!string.IsNullOrEmpty(searchText) && !string.IsNullOrEmpty(cellText))
{
return cellText.ToLower().StartsWith(searchText.ToLower());
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
}
Xaml:
<Window x:Class="WpfApplication17.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication17"
Title="MainWindow" Height="350" Width="525" Name="UI">
<StackPanel DataContext="{Binding ElementName=UI}">
<TextBox Name="SearchBox" />
<DataGrid x:Name="grid" local:DataGridTextSearch.SearchValue="{Binding ElementName=SearchBox, Path=Text, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding TestData}" >
<DataGrid.Resources>
<local:SearchValueConverter x:Key="SearchValueConverter" />
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="local:DataGridTextSearch.IsTextMatch">
<Setter.Value>
<MultiBinding Converter="{StaticResource SearchValueConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="Content.Text" />
<Binding RelativeSource="{RelativeSource Self}" Path="(local:DataGridTextSearch.SearchValue)" />
</MultiBinding>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="local:DataGridTextSearch.IsTextMatch" Value="True">
<Setter Property="Background" Value="Orange" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
</DataGrid>
</StackPanel>
</Window>
Result:
Edit:
If you just want to select the row based on a single Column you can modify quite easily :).
Override the Style of DataGridRow instead of DataGridCell.
<Style TargetType="{x:Type DataGridRow}">
First pass in the property you want into the IMultiValueConverter this should be your DataContext
<MultiBinding Converter="{StaticResource SearchValueConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="DataContext.MyProperty" />
<Binding RelativeSource="{RelativeSource Self}" Path="(local:DataGridTextSearch.SearchValue)" />
</MultiBinding>
Then change the Trigger to set IsSelected on the Row
<Style.Triggers>
<Trigger Property="local:DataGridTextSearch.IsTextMatch" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
</Style.Triggers>
Should look like this:
<DataGrid x:Name="grid" local:DataGridTextSearch.SearchValue="{Binding ElementName=SearchBox, Path=Text, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding TestData}" >
<DataGrid.Resources>
<local:SearchValueConverter x:Key="SearchValueConverter" />
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="local:DataGridTextSearch.IsTextMatch">
<Setter.Value>
<MultiBinding Converter="{StaticResource SearchValueConverter}">
<Binding RelativeSource="{RelativeSource Self}" Path="DataContext.MyProperty" />
<Binding RelativeSource="{RelativeSource Self}" Path="(local:DataGridTextSearch.SearchValue)" />
</MultiBinding>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="local:DataGridTextSearch.IsTextMatch" Value="True">
<Setter Property="IsSelected" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
</DataGrid>
Result:
I have been using MVVM for quite a while now, and I still prefer using it as a guideline rather than a strict practice, partly because it isn't always practical to do everything in MVVM pattern exactly, and even more so if you are not too familiar with it.I would suggest just playing around with it until you manage to find a form of MVVM that suits you.
I don't believe it is taboo to have Code in the Code Behind of the MVVM if the code is UI related. ScrollIntoView isn't a Bindable property so if you want to bind to it you will have to create a dependency Property to indirectly handle the binding. As for setting the selected item you could do it through something like:
View:
<TextBox Height="23" Text={Binding Path=Selected, UpdateSourceTrigger=PropertyChanged} HorizontalAlignment="Left" Margin="90,147,0,0" Name="textBox1" VerticalAlignment="Top" Width="120" />
<DataGrid AutoGenerateColumns="True"
ItemsSource="{Binding Path=ItemList}"
SelectedItem="{Binding Path=Selected}" >
</DataGrid>
ViewModel:
private string _selected = "";
public string Selected
{
get{ return _selected; }
set
{
if(_selected == value) return;
_selected = value;
base.OnPropertyChanged("Selected");
}
}

Setting Label.Content to Control on DataTrigger

recently I've tried to make label style which would allow to display image or textblock depending on property being set or not. I've bound proper objects to labels' DataContext and prepared reusable style for these labels. Default content is textblock with Name as its text but if IsIconSet property is true, then content would change into image with corresponding IconPath as source.
Similar approach works perfectly with label's properties like background or cursor but in described scenario it breaks up when IsIconSet has the same value in both instances. Then it displays nothing for first label and correct textblock/image for second label.
I've tried to attach converter to Name and IconPath bindings in style in order to check what value is being passed but it seems that it isn't even invoked on first label.
Has anyone managed to do something similar? Am I missing something fundamental? Or maybe there is another approach for such behaviour?
Any help will be appreciate.
Simplified code:
MainWindow
<StackPanel DataContext="{Binding First}">
<Label Style="{StaticResource LabelStyle}" />
</StackPanel>
<StackPanel DataContext="{Binding Second}">
<Label Style="{StaticResource LabelStyle}" />
</StackPanel>
Style
<Style x:Key="LabelStyle" TargetType="Label">
<Setter Property="Content">
<Setter.Value>
<TextBlock Text="{Binding Name}"/>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsIconSet}" Value="True">
<Setter Property="Content">
<Setter.Value>
<Image Source="{Binding IconPath}"/>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
Classes
public class ViewModel : INotifyPropertyChanged
{
private LabelClass _first;
private LabelClass _second;
public LabelClass First
{
get => _first;
set
{
_first = value;
OnPropertyChanged();
}
}
public LabelClass Second
{
get => _second;
set
{
_second = value;
OnPropertyChanged();
}
}
public ViewModel()
{
First = new LabelClass("First", "Resources/first.png");
Second = new LabelClass("Second", "Resources/second.png");
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class LabelClass : INotifyPropertyChanged
{
private string _name;
private string _iconPath;
public string Name
{
get => _name;
set
{
_name = value;
OnPropertyChanged();
}
}
public string IconPath
{
get => _iconPath;
set
{
_iconPath = value;
OnPropertyChanged();
OnPropertyChanged("IsIconSet");
}
}
public bool IsIconSet => !string.IsNullOrEmpty(IconPath);
public LabelClass(string name, string iconPath = null)
{
Name = name;
IconPath = iconPath;
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
LabelStyle can be used by multiple Labels, but TextBlock and Image from Content setter are created just once, there is only one instance for all labels, but it cannot be displayed in multiple places. so it is displayed in only one.
to fix the issue use ContentTemplate, like shown below.
<Setter Property="Content" Value="{Binding}"/> line means that entire DataContext is considered as Label Content. Is is necessary for bindings in ContentTemplate
<Style x:Key="LabelStyle" TargetType="Label">
<Setter Property="Content" Value="{Binding}"/>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsIconSet}" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image Source="{Binding IconPath}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
alternatively, convert TextBlock and Image to non-shared resources:
<Image Source="{Binding IconPath}" x:Key="Img" x:Shared="False"/>
<TextBlock Text="{Binding Name}" x:Key="Txt" x:Shared="False"/>
<Style x:Key="LabelStyle" TargetType="Label">
<Setter Property="Content" Value="{StaticResource Txt}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsIconSet}" Value="True">
<Setter Property="Content" Value="{StaticResource Img}"/>
</DataTrigger>
</Style.Triggers>
</Style>

Categories