WPF - Any ideas why datatrigger not firing? - wpf

My listbox is defined below. "Properties" is a BindingList which I am changing one of the items but the Image style is not being updated. Any ideas what I might be missing?
<ListBox x:Name="lstProperties"
Margin="0,0,5,0"
ItemsSource="{Binding Properties}"
SelectedItem="{Binding CurrentProperty}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="16"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image>
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Source" Value="Images/HouseRed_16.png"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SuitableApplicationCount, Converter={StaticResource greaterThanConverter}, ConverterParameter=0}" Value="True">
<Setter Property="Source" Value="Images/HouseYellow_16.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding InterestedApplicationCount, Converter={StaticResource greaterThanConverter}, ConverterParameter=0}" Value="True">
<Setter Property="Source" Value="Images/HouseAmber_16.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding MatchedApplicationId, Converter={StaticResource isNullOrEmptyConverter}}" Value="False">
<Setter Property="Source" Value="Images/HouseGreen_16.png"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<TextBlock Grid.Column="1" VerticalAlignment="Center">
<TextBlock.Text>
<MultiBinding StringFormat="{}Id: {0}, Plot: {1}">
<Binding Path="Id" FallbackValue="" />
<Binding Path="Plot" FallbackValue=""/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Make your model implement INotifyPropertyChanged.
I included some sample code:
public class MyModel : ViewModelBase
{
private int _suitableApplicationCount;
public int SuitableApplicationCount
{
get { return _suitableApplicationCount; }
set
{
_suitableApplicationCount = value;
OnPropertyChanged("SuitableApplicationCount");
}
}
public int _interestedApplicationCount;
public int InterestedApplicationCount
{
get { return _interestedApplicationCount; }
set
{
_interestedApplicationCount = value;
OnPropertyChanged("InterestedApplicationCount");
}
}
public int? _matchedApplicationId;
public int? MatchedApplicationId
{
get { return _matchedApplicationId; }
set
{
_matchedApplicationId = value;
OnPropertyChanged("MatchedApplicationId");
}
}
}
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}

Related

TabControl view doesn't update on template change

I need to change the view of a TabControl's content on-the-fly.
I am guessing the best way to accomplish this is to define the view as a DataTemplate, and then change said template using a trigger.
In my test app, the background color is tied to the same data trigger as the template. The background color updates immediately upon making the radio button selection.
Expected behavior: The Tab Item Content / DataTemplate also updates immediately.
Actual Behavior: Tab content view does not update until the tab selection is changed.
Here's my Minimal, Complete, and Verifiable example:
Window XAML
<Window x:Class="ChangeView.Window1"
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"
Title="Window1" Height="350" Width="400">
<Window.Resources>
<DataTemplate x:Key="ContentTemplate1">
<Grid>
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding MyBlurb}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="ContentTemplate2">
<Grid>
<Label HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Content="{Binding MyHeader}" Background="Black" Foreground="White" FontSize="72"/>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.Style>
<Style TargetType="{x:Type Grid}">
<Style.Triggers>
<DataTrigger Binding="{Binding ViewType1}" Value="False">
<Setter Property="Background" Value="Chartreuse"/>
</DataTrigger>
<DataTrigger Binding="{Binding ViewType1}" Value="True">
<Setter Property="Background" Value="Bisque"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Margin="10,38,0,0" Text="Content Template:"/>
<RadioButton x:Name="radio1" Margin="120,40,0,0" Grid.ColumnSpan="2" Content="1" GroupName="ViewSelect" IsChecked="{Binding Path=ViewType1}"/>
<RadioButton Margin="170,40,0,0" Grid.ColumnSpan="2" Content="2" GroupName="ViewSelect"/>
<TabControl Grid.Row="1" ItemsSource="{Binding TabGroup}">
<TabControl.Style>
<Style TargetType="{x:Type TabControl}">
<Setter Property="Margin" Value="10"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ViewType1}" Value="True">
<Setter Property="ContentTemplate" Value="{DynamicResource ContentTemplate1}"/>
</DataTrigger>
<DataTrigger Binding="{Binding ViewType1}" Value="False">
<Setter Property="ContentTemplate" Value="{DynamicResource ContentTemplate2}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.Style>
<TabControl.ItemTemplate>
<DataTemplate>
<Border x:Name="headerBorder">
<Label Content="{Binding MyHeader}" FontSize="20"/>
</Border>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
</Grid>
</Window>
Code Behind
namespace ChangeView
{
using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window, INotifyPropertyChanged
{
public ObservableCollection<TabData> TabGroup { get; set; } = new ObservableCollection<TabData>();
private bool _viewType1 = true;
public bool ViewType1
{
get { return _viewType1; }
set { _viewType1 = value; RaisePropertyChanged(nameof(ViewType1)); }
}
public Window1()
{
TabGroup.Add(new TabData("♻️", "Recycle"));
TabGroup.Add(new TabData("⚔", "Swords"));
TabGroup.Add(new TabData("⚗", "Chemistry"));
TabGroup.Add(new TabData("🌵", "Cactus"));
TabGroup.Add(new TabData("👺", "Tengu"));
TabGroup.Add(new TabData("🐙", "Octopus"));
DataContext = this;
InitializeComponent();
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void RaisePropertyChanged(string propName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
public class TabData : INotifyPropertyChanged
{
private string _myHeader, _myBlurb;
public TabData(string header, string blurb)
{
MyHeader = header;
MyBlurb = blurb;
}
public string MyHeader
{
get { return _myHeader; }
set { _myHeader = value; RaisePropertyChanged(nameof(MyHeader)); }
}
public string MyBlurb
{
get { return _myBlurb; }
set { _myBlurb = value; RaisePropertyChanged(nameof(MyBlurb)); }
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void RaisePropertyChanged(string propName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propName));
}
}
}
After changing the radio button state, change the selected tab. You will then see the correct content template.
It looks as if, in a TabControl, changing the content template alone does not cause the content to be rendered. If you render new content by switching the selected tab, the current content template will then be used.
So let's write one ContentTemplate, which creates a ContentControl and switches the ContentControl's ContentTemplate. I've tested, and the ContentControl will re-render its content when its ContentTemplate changes. The bindings get a little bit verbose.
<TabControl ItemsSource="{Binding TabGroup}" Grid.Row="1">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl
x:Name="ContentCtl"
Content="{Binding}"
/>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding DataContext.ViewType1, RelativeSource={RelativeSource AncestorType=TabControl}}"
Value="True">
<Setter
TargetName="ContentCtl"
Property="ContentTemplate"
Value="{DynamicResource ContentTemplate1}"
/>
</DataTrigger>
<DataTrigger
Binding="{Binding DataContext.ViewType1, RelativeSource={RelativeSource AncestorType=TabControl}}"
Value="False"
>
<Setter
TargetName="ContentCtl"
Property="ContentTemplate"
Value="{DynamicResource ContentTemplate2}"
/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</TabControl.ContentTemplate>
<TabControl.ItemTemplate>
<DataTemplate>
<Border x:Name="headerBorder">
<Label Content="{Binding MyHeader}" FontSize="20"/>
</Border>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
You could also do something ugly in your code behind to make the TabControl render itself again on command. Or maybe you can replace the metadata on TabControl.ContentTemplate.

Listbox item trigger colour change

I am trying to change the colour of the items in a listbox based on a trigger using MVVM
<Border Grid.Row="1" Width="300" Margin="0,0,20,0" BorderThickness="1,2,1,1" CornerRadius="5" BorderBrush="#FF999393" Background="#FFE9EDF1" >
<ListBox ItemsSource="{Binding LogMessageList, UpdateSourceTrigger=PropertyChanged}" Background="{x:Null}" Margin="3" BorderBrush="{x:Null}" FontSize="13.333" >
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding FatalError, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Firebrick" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I'm setting the property change correctly but nothing seems to change.
Thanks
EDIT:
Still stuck. Trying
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},Path=DataContext.FatalError, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Firebrick" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
Tried your scenario. Please refer the below code example:
View Model
public class Vm : INotifyPropertyChanged
{
public ObservableCollection<VmUser> VmUsers { get; set; }
private string errorType;
public string ErrorType
{
get { return errorType; }
set
{
errorType = value;
Raise("ErrorType");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void Raise(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
public class VmUser
{
public string Name { get; set; }
public int Age { get; set; }
}
Set the DataContext of window in constructor :
public MainWindow()
{
Vm = new Vm
{
VmUsers = new ObservableCollection<VmUser>
{
new VmUser { Name = "Gil", Age = 1 },
new VmUser { Name = "Dan", Age = 2 },
new VmUser { Name = "John", Age = 3 },
},
ErrorType = "Fatal"
};
InitializeComponent();
DataContext = TheVm;
}
Defined ListBox in XAML as:
<ListBox Grid.Row="2" ItemsSource="{Binding VmUsers}" DisplayMemberPath="Name" SelectedValuePath="Age">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="#FF403E3E" />
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},Path=DataContext.ErrorType, UpdateSourceTrigger=PropertyChanged}" Value="Fatal">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
To test the scenario I have wrote a small test to toggle the ErrorType on click on button
private void Button_Click(object sender, RoutedEventArgs e)
{
var vm = this.DataContext as Vm;
if (vm != null)
{
if (vm.ErrorType == "Fatal")
{
vm.ErrorType = "Non Fatal";
}
else
{
vm.ErrorType = "Fatal";
}
}
}
With above example foreground color changes based on ErrorType.

WPF ContentControl content set in DataTemplate trigger randomly resets to default content

I'm having an issue with the content of a ContentControl whose content was set using DataTriggers randomly resetting the content to the default content specified in the DataTemplate.
The scenario is that I have a bunch of devices (sensors) out on the network that I need to check status on, among other things. Depending on what state the sensor is in I may want to show a colored circle (green, red or yellow), or an image. For example if the sensor is in use by someone I want to show an image representing a user. If the sensor is available for connection I want to show a green ellipse, etc.
I'm currently using a WPF DataGrid to display a list of sensors and their status, though I get the same erroneous behavior with a ListBox and ListView (haven't tried plain ItemsControl). FYI, sensors come and go asynchronously.
What you'll see if you run the sample code is that initially items with a connection state of CONNECTED will first display with the desired image. As rows get added to the grid the image randomly disappears and is replace with the default content specified in the DataTemplate. This problem only occurs when there is an image in the content. The other states work just fine.
Below is all the code (xaml, viewmodel, model) I think you'll need to see the behavior. Sorry for the amount of code posted below. I've tried to pair it down as much as possible to illustrate the issue. Hopefully the problem is obvious by looking at the XAML. The remaining source code will help you get it up and running more quickly if you so choose.
Here's the Window XAML:
<Window x:Class="StackOverflowGridIssue.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:StackOverflowGridIssue.Model"
xmlns:shape="http://schemas.microsoft.com/expression/2010/drawing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" Width="500"
Title="MainWindow" >
<Grid>
<DataGrid Name="_SensorsDataGrid" ItemsSource="{Binding Sensors}"
AutoGenerateColumns="False" HeadersVisibility="Column" >
<DataGrid.Columns>
<!-- Status -->
<DataGridTemplateColumn Header="Status" MinWidth="50"
Width="SizeToHeader" IsReadOnly="True">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl x:Name="myContent" Background="LimeGreen"
Width="25" Height="25">
<ContentControl.ToolTip>
<TextBlock Text="{Binding ConnectionState, Mode=OneWay}"
Foreground="Black" />
</ContentControl.ToolTip>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}" BasedOn="{x:Null}" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContentControl}">
<Grid>
<Ellipse Height="10" Width="10"
Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding Background}" />
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ContentControl.Style>
</ContentControl>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.NOT_FOUND}">
<Setter TargetName="myContent" Property="Background" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.AVAILABLE}">
<Setter TargetName="myContent" Property="Background"
Value="GREEN" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.CONNECTED}">
<Setter TargetName="myContent" Property="Content">
<Setter.Value>
<Image Source="Images/User.png" />
</Setter.Value>
</Setter>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- DEBUG Connection State -->
<DataGridTextColumn Header="DEBUG"
Binding="{Binding ConnectionState}" Width="SizeToCells" />
<!-- Sensor Name -->
<DataGridTextColumn Header="Sensor Name"
Binding="{Binding Name}" Width="SizeToCells" />
<!-- IPAddress -->
<DataGridTextColumn Header="IP Address"
Binding="{Binding IPAddress}" Width="SizeToCells" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Here's the App.xaml.cs where I bootstrap everything and simulate sensors being discovered asynchronously (fyi, the same issue occurs if I load them serially, it's just easier to see when the sensors are loaded slowly one at a time:
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public const int NUM_SENSORS = 100;
Random _connectionStateGenerator = new Random();
ConnectionStateType _connectionState = ConnectionStateType.AVAILABLE;
SensorViewModel viewModel;
Timer _timer = new Timer(300);
int _index = 1;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// Get a handle to the main view (MainWindow)
Window window = new MainWindow();
viewModel = new SensorViewModel();
//Loads sensors synchronously (same issue)
//AddSensors(viewModel.Sensors);
window.DataContext = viewModel;
window.Show();
//Simulate async sensor discovery (more real world example)
_timer.Enabled = false;
_timer.AutoReset = true;
_timer.Elapsed += (s, args) =>
{
Dispatcher.InvokeAsync(new Action( () =>
{
viewModel.Sensors.Add(
new Sensor("Sensor" + _index, "192.168.1." + _index,
(ConnectionStateType)_connectionStateGenerator.Next(0, 3)));
if (_index++ > NUM_SENSORS)
_timer.Enabled = false;
}));
};
_timer.Enabled = true;
}
//Helper for loading synchronously rather than asynchronously
private void AddSensors(ObservableCollection<Model.Sensor> sensors)
{
for (int i = 0; i < NUM_SENSORS; i++)
{
_connectionState = (ConnectionStateType)_connectionStateGenerator
.Next(0, 5);
sensors.Add(
new Sensor("Sensor" + i, "192.168.1." + i, _connectionState));
}
}
}
Here's the model code representing a sensor:
public enum ConnectionStateType
{
NOT_FOUND,
AVAILABLE,
CONNECTED,
}
public class Sensor : INotifyPropertyChanged
{
string _name = "Unknown";
string _IPAddress;
ConnectionStateType _connectionState = ConnectionStateType.AVAILABLE;
public Sensor(string name, string IPAddress, ConnectionStateType connectionState)
{
_name = name;
_IPAddress = IPAddress;
_connectionState = connectionState;
}
public ConnectionStateType ConnectionState
{
get { return _connectionState; }
set
{
if (value == _connectionState) return;
_connectionState = value;
NotifyPropertyChanged("ConnectionState");
}
}
public string Name
{
get { return _name; }
set
{
if (value == _name) return;
_name = value;
NotifyPropertyChanged("Name");
}
}
public string IPAddress
{
get { return _IPAddress; }
set
{
if (value == _IPAddress) return;
_IPAddress = value;
NotifyPropertyChanged("IPAddress");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string property)
{
var handler = PropertyChanged;
if(handler != null)
handler(this, new PropertyChangedEventArgs(property));
}
}
Here's the View Model:
public class SensorViewModel : INotifyPropertyChanged
{
ObservableCollection<Sensor> _sensors = new ObservableCollection<Sensor>();
public ObservableCollection<Sensor> Sensors
{
get { return _sensors; }
private set
{
if (value == _sensors) return;
_sensors = value;
NotifyPropertyChanged("Sensors");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string property)
{
var handler = PropertyChanged;
if(handler != null)
handler(this, new PropertyChangedEventArgs(property));
}
}
Thanks for your help.
The solution ended up being to have the trigger swap out the entire template rather than trying to set the Content area in the template.
Here is a cleaner piece XAML with template resources defined and a DataGrid that uses those resources:
<UserControl x:Class="StackOverflowGridIssue.SensorGrid"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:model="clr-namespace:StackOverflowGridIssue.Model"
xmlns:shape="http://schemas.microsoft.com/expression/2010/drawing"
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">
<UserControl.Resources>
<ControlTemplate x:Key="DefaultConnectionStateTemplate"
TargetType="{x:Type ContentControl}">
<Grid>
<Ellipse Height="10" Width="10"
Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding Background}" />
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="ConnectedTemplate"
TargetType="{x:Type ContentControl}" x:Shared="false">
<Grid>
<Image Source="Images/User.png" Height="25" Width="25"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
<DataTemplate x:Key="SensorConnectionStateTemplate">
<ContentControl x:Name="myContent">
<ContentControl.ToolTip>
<TextBlock Text="{Binding ConnectionState, Mode=OneWay}" />
</ContentControl.ToolTip>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}" BasedOn="{x:Null}" >
<Setter Property="Template"
Value="{StaticResource DefaultConnectionStateTemplate}" />
</Style>
</ContentControl.Style>
</ContentControl>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.NOT_FOUND}">
<Setter TargetName="myContent" Property="Background" Value="Red" />
<Setter TargetName="myContent" Property="Content" Value="Not Found" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.AVAILABLE}">
<Setter TargetName="myContent" Property="Background" Value="GREEN" />
<Setter TargetName="myContent" Property="Content" Value="Available" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionState, Mode=OneWay}"
Value="{x:Static model:ConnectionStateType.CONNECTED}">
<Setter TargetName="myContent" Property="Template"
Value="{StaticResource ConnectedTemplate}" />
<Setter TargetName="myContent" Property="Content" Value="Connected" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</UserControl.Resources>
<Grid>
<DataGrid Name="_SensorsDataGrid" AutoGenerateColumns="False"
ItemsSource="{Binding Sensors}"
HeadersVisibility="Column" >
<DataGrid.Columns>
<!-- Status -->
<DataGridTemplateColumn Header="Status" MinWidth="50"
Width="SizeToHeader" IsReadOnly="True"
CellTemplate="{StaticResource SensorConnectionStateTemplate}" />
<!-- DEBUG Connection State -->
<DataGridTextColumn Header="DEBUG"
Binding="{Binding ConnectionState}" Width="SizeToCells" />
<!-- Sensor Name -->
<DataGridTextColumn Header="Sensor Name"
Binding="{Binding Name}" Width="SizeToCells" />
<!-- IPAddress -->
<DataGridTextColumn Header="IP Address" Width="SizeToCells"
Binding="{Binding IPAddress}" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

WPF Change ViewModel Property in View

I have a WPF DataGrid with several Templated Columns containing Checkboxes.
If I press a specific Checkbox the source of another checkbox has to be set.
I have a Viewmodel with a Property called Property (yes i know :)), and this Property has the Property "Visible".
If I check the "Mandatory" checkbox I want the "Visible" Value to be set and so the visible Checkbox to be set too, but somehow this is not working.
Heres my code:
<toolkit:DataGridTemplateColumn Header="Mandatory" IsReadOnly="False">
<toolkit:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<CheckBox IsChecked="{Binding Path=Mandatory,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
<CheckBox.Style>
<Style TargetType="{x:Type CheckBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=MandatoryDB}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=MandatoryDB}" Value="False">
<Setter Property="IsEnabled" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Checked">
<ei:ChangePropertyAction TargetObject="{Binding UpdateSourceTrigger=PropertyChanged}" PropertyName="ReadOnly" Value="False" />
<ei:ChangePropertyAction TargetObject="{Binding NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}" PropertyName="Visible" Value="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</CheckBox>
</StackPanel>
</DataTemplate>
</toolkit:DataGridTemplateColumn.CellTemplate>
</toolkit:DataGridTemplateColumn>
Thanks,
Jonny
I think what you're trying to do is to drive your business logic by your View which is a horrible anti-pattern. What you need to is when the value of the "Mandatory" checkbox is changed then your viewmodel needs to set other properties accordingly and alert your view that there was a change (INotifyPropertyChanged).
Your view should not set any viewmodel properties, it should only read them and pass user input, the only other things which your view needs to controll are converters and other view-only items.
Hope this helps
see if this can help you.
lets assume that you want to show yes, no and ans in your Datagrid. for this find the sample code below.
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Coll}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Yes">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox Width="50"
Height="50"
IsChecked="{Binding Yes,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="No">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox Width="50"
Height="50"
IsChecked="{Binding No,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Ans">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Rectangle Width="100"
Height="50"
RadiusX="25"
RadiusY="25">
<Rectangle.Style>
<Style TargetType="{x:Type Rectangle}">
<Style.Triggers>
<DataTrigger Binding="{Binding Ans}" Value="true">
<Setter Property="Fill" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding Ans}" Value="false">
<Setter Property="Fill" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
code behind
public partial class YesNoTest : Window
{
public YesNoTest()
{
InitializeComponent();
if (Coll == null)
{
Coll = new ObservableCollection<Anss>();
for (int index = 0; index < 10; index++)
{
Coll.Add(new Anss() { Yes = false, No = false, Ans = true });
}
}
this.DataContext = this;
}
private ObservableCollection<Anss> _coll;
public ObservableCollection<Anss> Coll
{
get { return _coll; }
set { _coll = value; }
}
}
public class Anss : INotifyPropertyChanged
{
private bool _Yes;
public bool Yes
{
get
{
return _Yes;
}
set { _Yes = value; OnChanged("Yes"); OnChanged("Ans"); }
}
private bool _No;
public bool No
{
get
{
if (_No == true)
Yes = false;
return _No;
}
set { _No = value; OnChanged("No"); OnChanged("Ans"); }
}
private bool _Ans;
public bool Ans
{
get
{
if (Yes == true)
_Ans = true;
else
_Ans = false;
return _Ans;
}
set { _Ans = value; OnChanged("Ans"); }
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnChanged(string name)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}

WPF error template not showing

I get no binding errors and this code works at another place. I haven't found out yet what I do now differently to the code where it works and it's not that much code.
In UserControl.Resource:
<Style TargetType="TextBox">
<Setter Property="BorderBrush" Value="DarkBlue"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="0,1,0,1"/>
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<AdornedElementPlaceholder/>
<Grid Margin="2,0,0,0">
<Ellipse Width="20" Height="20" Fill="Red"/>
<TextBlock Foreground="White" Text="X" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
Below in Xaml too:
<TextBlock Height="23" HorizontalAlignment="Left" Margin="22,90,0,0"
Text="Keywords" VerticalAlignment="Top"/>
<TextBox Height="23" HorizontalAlignment="Left" Margin="22,108,0,0"
VerticalAlignment="Top" Width="244">
<Binding Path="Tags" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<DataErrorValidationRule ValidatesOnTargetUpdated="False"/>
</Binding.ValidationRules>
</Binding>
</TextBox>
The button SAVE in my ViewModel is only activated when the Model.Tags property is longer 10 chars input from the user. The button activation/disable works fine when I enter 10,11 and then back 8 chars. All the property changes are fired.
Model:
namespace TBM.Model
{
public class Document : EntityBase , IDataErrorInfo
{
public int Id { get; set; }
public string DocumentName { get; set; }
public string Tags { get; set; }
public byte[] DocumentData { get; set; }
public int PeriodId { get; set; }
string IDataErrorInfo.Error { get { return null; } }
string IDataErrorInfo.this[string propertyName]
{
get { return this.GetValidationError(propertyName); }
}
public bool IsValid
{
get
{
foreach (string property in ValidatedProperties)
if (GetValidationError(property) != null)
return false;
return true;
}
}
static readonly string[] ValidatedProperties = { "Tags", };
private string GetValidationError(string propertyName)
{
if (Array.IndexOf(ValidatedProperties, propertyName) < 0)
return null;
string error = null;
switch (propertyName)
{
case "Tags": error = this.IsTagsEmpty(Tags); break;
default:
Debug.Fail("Unexpected property being validated on Document: " + propertyName);
break;
}
return error;
}
private string IsTagsEmpty(string value)
{
if (value != null && value.Trim().Length >= 10)
return null;
else
return "The keywords must have at least 10 chars!";
}
}
}
ViewModel:
public RelayCommand SaveDocumentCommand
{
get { return _saveDocumentCommand ?? (_saveDocumentCommand =
new RelayCommand(() => SaveDocument(),() => CanSaveDocument())); }
}
private bool CanSaveDocument()
{
return _document.IsValid;
}
//...
What does not work is the ErrorTemplate with the red Ellipse is not showing at all?
UPDATE: Exactly the below code works in a TEST project. But in my productive project It does not find the Resource??? Why the heck this?
<TextBlock Height="23" HorizontalAlignment="Left" Margin="22,89,0,0"
Text="Keywords" VerticalAlignment="Top"/>
<TextBox Style="{StaticResource bla}" Height="23" HorizontalAlignment="Left"
Margin="22,109,0,0" VerticalAlignment="Top" Width="244">
<Binding Path="Tags" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<DataErrorValidationRule ValidatesOnTargetUpdated="False"
ValidationStep="UpdatedValue"/>
</Binding.ValidationRules>
</Binding>
</TextBox>
<UserControl.Resources>
<Style x:Name="bla" TargetType="TextBox">
<Setter Property="BorderBrush" Value="DarkBlue"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="0,1,0,1"/>
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<AdornedElementPlaceholder/>
<Grid Margin="2,0,0,0">
<Ellipse Width="20" Height="20" Fill="Red"/>
<TextBlock Foreground="White" Text="X" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
I had similar problem. Fight it for hours just to realize that something was wrong with adorner layer.
What I did is put my with controls inside . And that was it. For some reason this decorator layer sometimes is gone. This is certainly true for TabControl (but in my case it was some other reason).
So it should look like this
<AdornerDecorator>
<Grid>
<TextBox .../>
</Grid>
</AdornerDecorator>
Hope this helps!
I have had a similar problem to this, it turned out not to be the template but the validation was not returning what I expected.
The ViewModel have to implement IDataErrorInfo, not the Model. The ViewModel binds to your View as the DataContext, not the Model, so implement the interface in ViewModel and bind to corresponding properties in XAML.

Resources