Where to raise NotifyPropertyChanged? - c#

I just implemented my business logic validation according to Rachel Lim's blog.
Everything was running great until I decided to put a trigger in my view bound to the IsValid property like this:
<ListBox ItemsSource="{Binding EdgedBoards}" SelectedItem="{Binding SelEdgedBoard, Mode=TwoWay}" DisplayMemberPath="Name">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="Focusable" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsValid}" Value="False">
<Setter Property="Focusable" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The problem is that when the bound item has an error (SelEdgedBoard.IsValid == false) the trigger is not notified to reevaluate and the item keeps its focusable property to true.
I've already tried to put a NotifyPropertyChanged("IsValid") before the GetValidationError() returns its value, but this way I get a stackoverflow exception:
#region IsValid Property
public bool IsValid
{
get
{
return string.IsNullOrWhiteSpace(GetValidationError());
}
}
public string GetValidationError()
{
string error = null;
if (ValidatedProperties != null)
{
foreach (string s in ValidatedProperties)
{
error = GetValidationError(s);
if (!string.IsNullOrWhiteSpace(error))
{
break;
}
}
}
NotifyPropertyChanged("IsValid");
return error;
}
#endregion

Of course it causes a stack overflow. When you do:
NotifyPropertyChanged("IsValid")
You force the WPF infrastructure to reevaluate the value of IsValid. It does this by calling the IsValid getter, which, in turn, calls GetValidationError again!
There are several ways you can handle this. I would probably create a private member variable to hold the last value of IsValid and then compare the current value to the old value before calling NotifyPropertyChanged.
Another way might be to only call NotifyPropertyChanged("IsValid") when one of your validated properties changes (so in the setter of each of those properties) as this is what might cause a change in IsValid.

private bool _isValid;
public bool IsValid
{
get
{
string.IsNullOrWhiteSpace(GetValidationError())
return _isValid;
}
set
{
_isValid = value;
NotifyPropertyChanged("IsValid");
}
}
public string GetValidationError()
{
string error = null;
if (ValidatedProperties != null)
{
foreach (string s in ValidatedProperties)
{
error = GetValidationError(s);
if (!string.IsNullOrWhiteSpace(error))
{
break;
}
}
}
IsValid=string.IsNullOrWhiteSpace(error);
return error;
}
I hope this will help.

Related

WPF DataGridHyperlinkColumn: hiding or showing hyperlink depending on a condition

I have an WPF MVVM app. In the main WPF window, I have a WPF datagrid which has some columns. One of them is of type DataGridHyperlinkColumn. This column contains a link which does some stuff on user click.
The datagrid is bound to a data model class, let's say MyDataGridModel (I am only showing here the necessary objects to understand my case) :
public class MyDataGridModel : NotifyBase
{
// Properties, members, methods, etc.
public MyLinkData myLinkData { get; set; }
}
The DataGridHyperlinkColumn column is bound to a data model, let's say MyLinkData:
MyLinkData:
public class MyLinkData
{
private string linkText = string.Empty;
private string linkValue = string.Empty;
public string LinkText
{
get
{
return this.linkText;
}
private set
{
if (!this.linkText.Equals(value))
{
this.linkText = value;
}
}
}
public string LinkValue
{
get
{
return this.linkValue;
}
private set
{
if (!this.linkValue.Equals(value))
{
this.linkValue = value;
}
}
}
#region Constructors
public MyLinkData(string linkText, string linkValue)
{
this.LinkText = linkText;
this.LinkValue = linkValue;
}
#endregion
}
View:
<my:DataGridHyperlinkColumn
Binding="{Binding myLinkData.LinkValue}"
ContentBinding="{Binding myLinkData.LinkText}">
<my:DataGridHyperlinkColumn.ElementStyle>
<Style TargetType="TextBlock">
<EventSetter Event="PreviewMouseLeftButtonDown" Handler="OnCellHyperlinkClick" />
</Style>
</my:DataGridHyperlinkColumn.ElementStyle>
<my:DataGridHyperlinkColumn.CellStyle>
<Style TargetType="my:DataGridCell">
<Style.Resources>
<Style TargetType="Hyperlink">
<Style.Triggers>
<DataTrigger Binding="{Binding myLinkData.LinkValue}" Value="">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</Style.Resources>
</Style>
</my:DataGridHyperlinkColumn.CellStyle>
</my:DataGridHyperlinkColumn>
This link is disabled when link value contains an empty string and enabled otherwise.
Now, what I would like to do is to hide the link when link value contains an empty string instead of making it disabled. So how can I do this?
I was thinking of changing link color to make it not visible but then I may take into account when the row is selected (color is blue) and some other things making things more difficult (in my case rows also change color depending on some conditions).

DataGrid row highlighting for dynamic objects

I’m using the „Xceed Extended WPF Tookit Plus (Complete”) Edition in version 2.4.14475.10340 and the DataGrid of the toolkit in a production software environment.
Currently I’m struggling to get a datarow highlighting to work again.
a.) The data source for the grid has been a standard DataTable, so a custom DataRow style like the following did work for me:
<Style TargetType="{x:Type xcdg:DataRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=[IsTrend]}" Value="True">
<Setter Property="Background" Value="Gold"/>
</DataTrigger>
</Style.Triggers>
</Style>
Because of the dynamic nature of the underlying data, we changed the source type to “ExpandoObject” and use that in its dictionary form to add/remove data entries, which then are mapped to datagrid columns using the field name. After that change the above styling doesn’t work anymore, error see below:
Cannot get 'Item[]' value (type 'Object') from '' (type 'ExpandoObject'). BindingExpression:Path=[IsTrend]; DataItem='ExpandoObject' (HashCode=34521593); target element is 'DataRow' (Name=''); target property is 'NoTarget' (type 'Object') TargetInvocationException:'System.Reflection.TargetInvocationException
b.) As a workaround we changed the style to a custom converter:
<Style TargetType="{x:Type xcdg:DataRow}">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource HighlightingConverter}}"/>
</Style>,
which returns a Brush, depending on the value of the fields of the DataRow item (DataGrid.GetItemFromContainer(row)), so the ExpandoObject.
Now the rows in the datagrid are sometimes colored, but a.) not correctly (wrong line) or b.) loose color after a refresh of the data. It looks like a datarow container is colored and then re-used for new data.
My question(s):
Why does it work with the above style in a.), but not if I use a custom converter like in b.)? Which field or path is accessed through using the “[]” sytax?
Is there a way to achieve row lighting for a dynamic data source?
/Edit:
The source collections looks like this.
private ObservableCollection<dynamic> GetDynamicOrders2()
{
var retVal = new ObservableCollection<dynamic>();
for (int i = 0; i < 50; i++)
{
dynamic eo = new ExpandoObject();
eo.Name = new CellContent("Order" + i);
eo.IsTrend = new CellContent(i % 2 == 0);
var converted = (IDictionary<string, object>)eo;
converted["Number"] = new CellContent(i % 4);
converted["NumberDouble"] = new CellContent((double)i);
converted["properties_name_first"] = new CellContent("Name " + i);
retVal.Add(eo);
}
return retVal;
}
A cell object is defined as:
public sealed class CellContent : INotifyPropertyChanged
{
private object _value;
public object Value
{
get { return _value; }
set
{
if (Equals(value, _value)) return;
_value = value;
OnPropertyChanged();
OnPropertyChanged("DisplayValue");
OnPropertyChanged("BackgroundColor");
OnPropertyChanged("ForegroundColor");
}
}
public bool Meta { get; set; }
public string Format { get; set; }
public double Threshold { get; set; }
public string DisplayValue
{
get
{
return string.Format(string.Format("{{0:{0}}}", Format), Value);
}
}
public SolidColorBrush BackgroundColor
{
get
{
var defaultColor = new SolidColorBrush(Colors.Transparent);
var valueType = Value.GetType();
if (valueType != typeof (double)) return defaultColor;
if (double.IsNaN(Threshold)) return defaultColor;
return (double)Value >= Threshold ? new SolidColorBrush(Colors.Red) : defaultColor;
}
}
public SolidColorBrush ForegroundColor
{
get
{
var defaultColor = new SolidColorBrush(Colors.Black);
var valueType = Value.GetType();
if (valueType != typeof(double)) return defaultColor;
// inactive
if (double.IsNaN(Threshold)) return defaultColor;
return (double)Value >= Threshold ? new SolidColorBrush(Colors.Blue) : defaultColor;
}
}
public CellContent(object value, bool meta = false, string format = "", double threshold = double.NaN)
{
Value = value;
Meta = meta;
Format = format;
Threshold = threshold;
}
private bool Equals(CellContent other)
{
return Equals(Value, other.Value);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((CellContent)obj);
}
public override int GetHashCode()
{
return (Value != null ? Value.GetHashCode() : 0);
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
/Edit2: binding error if I use the explicit path:
System.Windows.Data Error: 40 : BindingExpression path error: 'IsTrend' property not found on 'object' ''MainWindow' (Name='')'. BindingExpression:Path=IsTrend; DataItem='MainWindow' (Name=''); target element is 'DataRow' (Name=''); target property is 'NoTarget' (type 'Object')
/Edit3: Solution.
The whole problem was in the way I added the style. As in the question above I defined the style globally, so it is applied to each single DataRow object in this user control/window. That leads to a datacontext of "MainWindow", which is of course of no greater use here.
I changed the style to (key added, path corrected)
<Style TargetType="{x:Type xcdg:DataRow}" x:Key="RowStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsTrend.Value}" Value="True">
<Setter Property="Background" Value="Gold"/>
</DataTrigger>
</Style.Triggers>
</Style>
and added a reference from the datagrid like that
<xcdg:DataGridControl x:Name="DataGrid" ItemContainerStyle="{StaticResource RowStyle}">
Done. It works again!
All help is greatly appreciated.
Thank in advance,
Nico Ecker
Let me break it down into three questions:
Which field or path is accessed through using the “[]” syntax?
The “[]” syntax is used to access the Indexer of an object. Whatever you put between the brackets will be treated as the indexer's argument(s).
Why does it work with the above style in a.), but not if I use a custom converter like in b.)?
I'm not sure about the second part of that question - it would require additional investigation, which hopefully will not be necessary. As for why does it work if you use DataTable as items source - the data being presented by each row is a DataRow, and your trigger binds to it's Item[String] indexer. The code-behind equivalent of accessing that data would be like
var isTrend = dataRow["IsTrend"];
Is there a way to achieve row lighting for a dynamic data source?
I'm assuming that when you generate the data you set an IsTrend property on your ExpadoObject:
dataObject.IsTrend = (...);
If that's the case, the easiest thing you can do is to set up your binding to access that property rather than use an indexer:
<DataTrigger Binding="{Binding Path=IsTrend}" (...) />
The reason why the previous binding does not work is because ExpandoObject does not have a public Item[String] indexer - it does have one, but it's an explicitly implemented IDictionary<String, Object>.Item[String], and therefore is not considered public. It's possible to bind to explicitly implemented interface members in XAML, but it's not that pretty and it would be especially hard because that particular interface is a generic one.
I've used your source and make it work with DataGrid. See below code:
XAML:
<Window x:Class="TabControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TabControl"
Title="MainWindow" Height="300" Width="300"
xmlns:Interact="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}"
>
<Window.Resources>
<Style TargetType="{x:Type DataGridRow}" x:Key="myStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding IsTrend.Value}" Value="True">
<Setter Property="Background" Value="Gold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<ScrollViewer>
<DataGrid ItemsSource="{Binding list}" x:Name="myGrid" RowStyle="{StaticResource myStyle}" >
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name.Value,Mode=TwoWay}" />
<DataGridTextColumn Header="IsTrend" Binding="{Binding Path=IsTrend.Value,Mode=TwoWay}" />
<DataGridTextColumn Header="Number" Binding="{Binding Path=Number.Value,Mode=TwoWay}" />
<DataGridTextColumn Header="properties_name_first" Binding="{Binding Path=properties_name_first.Value,Mode=TwoWay}" />
</DataGrid.Columns>
</DataGrid>
</ScrollViewer>
Output:
Suggestion:
If you want to remove .Value from every binding you can override the ToString() method in your CellContent class like below:
public override string ToString()
{
return Value.ToString();
}
and Binding will change like:
<DataGridTextColumn Header="Name" Binding="{Binding Path=Name,Mode=TwoWay}" />
PS: Though it has a problem, after removing .Value DataTrigger is not working. I'm working on that if I found any solution I'll update here.
Here is the solution for above mentioned problem.

Binding complex property of model to DataGridTextColumn and using Style to set value displayed

Let's say i have the code below...i have a Field class that holds a state and the actual value to display. I have model that defines an instance of this MyField class named Field1. In by DataGrid i am binding to this Field1 and using a style to display the value as well as color the background if IsStale is true. The problem is neither the value, nor the background is being colored. It appears the problem is that when used as is, the datacontext for the Style is MyData object and not in fact the MyField object, even though i specify the binding as "Field1". The error being printed out is "BindingExpression path error: 'IsStale' property not found on 'object' ''MyDataModel'".
How can properly bind to a complex property in a Datagrid's cell such that i can use multiple attributes of the bound model?
class MyField : BaseModel
{
private bool _isStale;
public bool IsStale
{
get { return _isStale; }
set
{
if (_isStale == value) return;
_isStale = value;
NotifyPropertyChanged("IsStale");
}
}
private double _value;
public double Value
{
get { return _value; }
set
{
if (_value.Equals(value)) return;
_value = value;
NotifyPropertyChanged("Value");
}
}
}
class MyDataModel
{
MyField Field1 {get; set;}
public MyData()
{
Field1 = new Field1();
//when ever underlying property of MyField changes, we need to fire property changed because xaml binds to Field1
Field1.PropertyChanged += (o,e) =>
{
MyField field = o as MyField;
if (field!=null)
NotifyPropertyChanged("Field1");
};
}
}
<DataGridTextColumn Header="Weight" Binding="{Binding Field1}" ElementStyle="{StaticResource DGCellStyle}"/>
Style:
<Style x:Key="DGCellStyle" TargetType="TextBlock">
<Setter Property="Width" Value="Auto"/>
<Setter Property="Text" Value="{Binding Value, Converter={StaticResource NumberFormatConverter}, ConverterParameter=0.00}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsStale}" Value="True">
<Setter Property="Background" Value="Pink"/>
</DataTrigger>
</Style.Triggers>
</Style>

Setting a treeview items text colour in realtime based on a ViewModel property

In my WPF application I am using the MVVM pattern. My view has a treeview which I bind an observableCollection of objects as defined below. What I want to do is to change the colour of a tree item name when the bound object sets it’s dirty property to true. I can get it to set the colour when I first populate the tree but then it doesn’t reflect the changes when the property changes between false and true.
public class HierarchicalItem
{
private readonly ObservableCollection<HierarchicalItem> _children = new ObservableCollection<HierarchicalItem>();
public ViewModelBase ViewModel { get; set; }
public string Name
{
get { return ViewModel.ViewModelName; }
}
public ICollection<HierarchicalItem> Children
{
get { return _children; }
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
if (_isSelected)
EventSystem.Publish(new SelectedViewModelMessage { SelectedViewModel = ViewModel });
}
}
public bool IsDirty
{
get { return ViewModel.IsDirty; }
}
}
This is the treeview xaml:
<TreeView Grid.Row="0" Grid.Column="0" ItemsSource="{Binding Path=Views}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="True"/>
<Setter Property="IsSelected" Value="{Binding Path=IsSelected, Mode=OneWayToSource}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:HierarchicalItem}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Name}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsDirty}" Value="True">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
Here is the collection that gets bound to the tree:
private readonly ObservableCollection<HierarchicalItem> _views = new ObservableCollection<HierarchicalItem>();
public ObservableCollection<HierarchicalItem> Views
{
get { return _views; }
}
The ViewModels that are referenced in the HierarchicalItem collection all derive from a base class that exposes the “IsDirty” property. This is definantly changing state so I’m not sure if I’ve made a coding mistake or if what I want to achieve can’t be done this way. The classes all use the “INotifyPropertyChanged” interface. Here is the “IsDirty” property in from the ViewModel base class:
public class ViewModelBase : ValidatableModel
{
#region Properties
private bool _isDirty;
public bool IsDirty
{
get { return _isDirty; }
protected set
{
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
.
.
.
Etc
It's because your HierarchicalItem (the one you are having issues with) does not use a full INPC approach for its IsDirty property. The viewmodel does, but that is not enough, as the DataTemplate will be using the IsDirty property of the HierarchicalItem, so that needs to be full INPC property too
Changed that to this and it should be ok.
private bool _isDirty;
public bool IsDirty
{
get { return _isDirty; }
protected set
{
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
Though for your use case you will need to figure out some way to fire that. Or another thing you could try would be to change the binding in HierarchicalItem DataTemplate to this
<DataTrigger Binding="{Binding ViewModel.IsDirty}" Value="True">

How to show a tooltip on invalid input into a textbox

I am trying to setup validation for a phone number field in a WPF application using MVVM. I have the textbox text bound but cant seem to figure out the logic involved in rejecting input and popping up a tooltip. Any suggestions would be appreciated.
[Required]
public string PhoneNumber
{
get
{
return EntityPhone.PhoneNumber;
}
set
{
int intValue = 0;
if(!int.TryParse(value, out intValue))
{
// ToolTip tt = new ToolTip();
// tt.Content = "Invalid Character. Please enter a valid 10-digit number";
}
EntityPhone.PhoneNumber = value;
NotifyOfPropertyChange(() => PhoneNumber);
}
}
First you'll want to make your class inherit IDataErrorInfo, which is used by WPF for validation purposes.
public class MyClass : IDataErrorInfo
{
...
#region IDataErrorInfo Members
string IDataErrorInfo.Error
{
get { return null; }
}
string IDataErrorInfo.this[string columnName]
{
get
{
if (columnName == "PhoneNumber")
{
// Validate property and return a string if there is an error
return "Some error";
}
// If there's no error, null gets returned
return null;
}
}
#endregion
}
Next, tell your binding that it should be validating the value when it changes
<TextBox Text="{Binding Path=PhoneNumber, ValidatesOnDataErrors=True}" ... />
And finally, create a validation template. Here's the style/template I usually use
<!-- ValidatingControl Style -->
<Style TargetType="{x:Type FrameworkElement}" x:Key="ValidatingControl">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding
Path=(Validation.Errors)[0].ErrorContent,
RelativeSource={x:Static RelativeSource.Self}}" />
</Trigger>
</Style.Triggers>
</Style>

Categories

Resources