problems with validation rule - wpf

I am trying to get a validation rule to return an error. I implemented IDataErrorInfo in my model, which contains my business object properties and messages to return in the event validation fails. I also created a validation rule. The problem is, the validation rule is firing (bookmarked it) but the IDataErrorInfo reference in the rule never has an error, even though the IDataErrorInfo implementation of my model generates one. The datagrid definitely shows there was a validation failure.
I tested it by having the rule and model return two different messages, and the model's version is always returned. It is like my rule cannot see what is in the IDataErrorInfo object, or it is just creating a new instance of it.
DataGrid:
<DataGrid ItemsSource="{Binding Path=ProjectExpenseItemsCollection}" AutoGenerateColumns="False"
Name="dgProjectExpenseItems" RowStyle="{StaticResource RowStyle}"
SelectedItem="{Binding Path=SelectedProjectExpenseItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
CanUserDeleteRows="True" CanUserAddRows="True">
<DataGrid.RowValidationRules>
<vr:RowDataInfoValidationRule ValidationStep="UpdatedValue" />
</DataGrid.RowValidationRules>
<DataGrid.Columns>
<DataGridTextColumn Header="Item Number"
Binding="{Binding ItemNumber, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</DataGrid.Columns>
</DataGrid>
Validation Rule:
The object "idei" isn't null, but idei.Error is always a zero-length string ("")
public class RowDataInfoValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
BindingGroup bindingGroup = (BindingGroup)value;
IDataErrorInfo idei = bindingGroup.Items[0] as IDataErrorInfo;
string error = (idei != null) ? idei.Error : null;
return (string.IsNullOrEmpty(error)) ? ValidationResult.ValidResult : new ValidationResult(false, error + ": ValidationRule");
}
}
Model/Business Object:
public class ProjectExpenseItemsBO : IDataErrorInfo, IEditableObject, INotifyPropertyChanged
{
public string ItemNumber { get; set; }
public ProjectExpenseItemsBO() {}
// string method
static bool IsStringMissing(string value)
{
return String.IsNullOrEmpty(value) || value.Trim() == String.Empty;
}
#region IDataErrorInfo Members
public string Error
{
get { return this[string.Empty]; }
}
public string this[string propertyName]
{
get
{
string result = string.Empty;
if (propertyName == "ItemNumber")
{
if (IsStringMissing(this.ItemNumber))
{
result = "Item number cannot be empty-IDataError!";
}
}
return result;
}
}
#endregion
}

The IDataErrorInfo object that the rule gets will be an instance of your ProjectExpenseItemsBO object. The only property you check is Error, which you have implemented to return this[string.Empty], which will always return string.Empty. You probably either want to change your implementation of the Error property to look at all the errors in the object, or to have RowDataInfoValidationRule iterate through properties and get the error message for each one through the indexer.
You are getting validation errors from the model because your binding to ItemNumber has ValidatesOnDataErrors set to True, so the framework will call the indexer with the property name ItemNumber and get your error message.

Related

Change GridViewColumn text color base on condition

This is my GridViewColumn:
<GridViewColumn Width="180" Header="Status">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="Txt" Text="{Binding Status}" Foreground="Yellow" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
The Status field is a property of my binding object and all i want to do is change this GridViewColumn color but this time base on condition:
I have another propgerty called StatusMessage which is simple enum:
public enum StatusMessage
{
InProcess,
Done,
Wait
}
So this enum property is changing all the time and for every value of this enum i want to define different color.
Is it possible ?
Edit
My View model class inherit from BaseObservableObject:
public class BaseObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
{
var propName = ((MemberExpression)raiser.Body).Member.Name;
OnPropertyChanged(propName);
}
protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
OnPropertyChanged(name);
return true;
}
return false;
}
}
My properties:
public string Status
{
get { return _status; }
set
{
_status = value;
OnPropertyChanged();
}
}
public StatusMsg StatusMessage
{
get { return _statusMsg; }
set {
_statusMsg = value;
OnPropertyChanged();
}
}
XAML:
<GridViewColumn Width="180" Header="Status" DisplayMemberBinding="{Binding Status}">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Foreground="{Binding StatusMsg,Converter={c:StatusMessageToColorConverter}}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
StatusMessageToColorConverter is the same as #grek40 suggested and still my TextBlock Foreground not changing.
I first focus on the value conversion, then I say something about the other requirement ("So this enum property is changing all the time and for every value of this enum i want to define different color.")
Since you have an enum value but you want to have a color specification, you need to convert the value. This can be done with the help of an implementation of the IConverter interface. There are different ways to reference a converter in XAML, I additionally inherit from MarkupExtension so I am able to access the converter directly.
public class StatusMessageToColorConverter : MarkupExtension, IValueConverter
{
// one way converter from enum StatusMessage to color
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is StatusMessage && targetType == typeof(Brush))
{
switch ((StatusMessage)value)
{
case StatusMessage.InProcess:
return Brushes.Yellow;
case StatusMessage.Done:
return Brushes.Green;
case StatusMessage.Wait:
return Brushes.Red;
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
public StatusMessageToColorConverter()
{
}
// MarkupExtension implementation
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
As you can see, I convert if the input value is of type StatusMessage and the target type is typeof(Brush), which is the Type of the Foreground property. I just chose some colors, you may use different colors or even more complex brushes if you like.
In XAML, the namespace of the converter needs to be referenced. In my case its just WpfApplication2 associated to the XAML namespace name c
<Window
[... other properties]
xmlns:c="clr-namespace:WpfApplication2">
Now, when binding to the foreground property, utilize the converter
<TextBlock Text="{Binding Status}" Foreground="{Binding StatusMessage,Converter={c:StatusMessageToColorConverter}}" />
This should do the trick.
Now to the other part about dynamically changing the value. Your viewmodel class needs to implement INotifyPropertyChanged and raise the PropertyChanged event whenever a value is changed. As an example, see the following class that only contains the two properties of your example and the necessary notification logic.
public class ItemModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
void NotifyPropertyChanged([CallerMemberName]string prop = null)
{
var tmp = PropertyChanged;
if (tmp != null) tmp(this, new PropertyChangedEventArgs(prop));
}
private string _Status;
public string Status
{
get { return _Status; }
set { _Status = value; NotifyPropertyChanged(); }
}
private StatusMessage _StatusMessage;
public StatusMessage StatusMessage
{
get { return _StatusMessage; }
set { _StatusMessage = value; NotifyPropertyChanged(); }
}
}
More complex viewmodels can follow the same approach. For less update overhead, compare the current value and the new value in the setter and only notify if the value actually changes.
In your view model you could add a property called GetColour that looks at the current enum value and returns a colour. Then just bind the GetColour property in your xaml.

How to resolve a "Binding Expression Path error" in WPF?

I'm binding an observsable collection of model objects to a data grid. But when I set the binding to the collection, I get a path error to the peoprties.
In debugging this issue, I've checked that the public properties in the CustomerModel are correctly named in the DataGrid binding. And also that the collection being returned to the model isn't empty. I also checked that the data context is set correctly in the View's code behind.
I think it might be an error due to the way I've specified the binding path in the xaml..
The full details of the binding error is as follows, for each field:
System.Windows.Data Error: 40 : BindingExpression path error: 'FirstName' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=FirstName; DataItem='MainViewModel' (HashCode=55615518); target element is 'TextBox' (Name='fNameTbx'); target property is 'Text' (type 'String')
System.Windows.Data Error: 40 : BindingExpression path error: 'LastName' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=LastName; DataItem='MainViewModel' (HashCode=55615518); target element is 'TextBox' (Name='lNameTbx'); target property is 'Text' (type 'String')
System.Windows.Data Error: 40 : BindingExpression path error: 'Email' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=Email; DataItem='MainViewModel' (HashCode=55615518); target element is 'TextBox' (Name='emailTbx'); target property is 'Text' (type 'String')
Could anyone point me in the right direction, in order to debug this further?
DataGrid binding path and source are set as follows:
<DataGrid Name="infogrid"
Grid.Row="0"
Grid.RowSpan="3"
Grid.Column="1"
Grid.ColumnSpan="3"
AutoGenerateColumns="False"
ItemsSource="{Binding Customers}"
SelectedItem="{Binding SelectedCustomer}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Customers.Id}" Header="ID" />
<DataGridTextColumn Binding="{Binding Customers.FirstName}" Header="First Name" />
<DataGridTextColumn Binding="{Binding Customers.LastName}" Header="Last Name" />
<DataGridTextColumn Binding="{Binding Customers.Email}" Header="Email" />
</DataGrid.Columns>
</DataGrid>
The View Model contains an Observable collection of type CustomerModel, called Customers. This is what I've set the DataGrid ItemSource to. (I've removed other code from VM for readability)
namespace MongoDBApp.ViewModels
{
class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private ICustomerDataService _customerDataService;
public MainViewModel(ICustomerDataService customerDataService)
{
this._customerDataService = customerDataService;
QueryDataFromPersistence();
}
private ObservableCollection<CustomerModel> customers;
public ObservableCollection<CustomerModel> Customers
{
get
{
return customers;
}
set
{
customers = value;
RaisePropertyChanged("Customers");
}
}
private void QueryDataFromPersistence()
{
Customers = _customerDataService.GetAllCustomers().ToObservableCollection();
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
And these are the fields that in the CustomerModel, so not sure why the properties are not being found during binding:
public class CustomerModel : INotifyPropertyChanged
{
private ObjectId id;
private string firstName;
private string lastName;
private string email;
[BsonElement]
ObservableCollection<CustomerModel> customers { get; set; }
/// <summary>
/// This attribute is used to map the Id property to the ObjectId in the collection
/// </summary>
[BsonId]
public ObjectId Id { get; set; }
[BsonElement("firstName")]
public string FirstName
{
get
{
return firstName;
}
set
{
firstName = value;
RaisePropertyChanged("FirstName");
}
}
[BsonElement("lastName")]
public string LastName
{
get
{
return lastName;
}
set
{
lastName = value;
RaisePropertyChanged("LastName");
}
}
[BsonElement("email")]
public string Email
{
get
{
return email;
}
set
{
email = value;
RaisePropertyChanged("Email");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This is how the data context is set in the View's code behind:
public partial class MainView : Window
{
private MainViewModel ViewModel { get; set; }
private static ICustomerDataService customerDataService = new CustomerDataService(CustomerRepository.Instance);
public MainView()
{
InitializeComponent();
ViewModel = new MainViewModel(customerDataService);
this.DataContext = ViewModel;
}
}
These binding errors are not related to your DataGrid.
They indicate that you have 3 TextBoxes somewhere of the names fNameTbx, lNameTbx, and emailTbx. A DataGrid does not generate it's items with a Name property, so it is not the one causing these binding errors.
When trying to read binding errors, it's best to break them up by semi-colons and read them backwards, as demonstrated here.
For example,
System.Windows.Data Error: 40 : BindingExpression path error: 'FirstName' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=FirstName; DataItem='MainViewModel' (HashCode=55615518); target element is 'TextBox' (Name='fNameTbx'); target property is 'Text' (type 'String')
Can also be read as
target property is 'Text' (type 'String')
target element is 'TextBox' (Name='fNameTbx');
DataItem='MainViewModel' (HashCode=55615518);
BindingExpression path error: 'FirstName' property not found on 'object' ''MainViewModel' (HashCode=55615518)'. BindingExpression:Path=FirstName;
Meaning somewhere you have
<TextBox Name="fNameTbx" Text="{Binding FirstName}" />
Where the DataContext of this TextBox is of type MainViewModel. And MainViewModel does not have a property of FirstName.
I'd recommend searching your project for those names, or you could use a tool like Snoop to debug databindings and DataContext issues at runtime.
The exceptions indicate that the DataBinding engine is looking for the fields FirstName, LastName, etc. on MainViewModel as opposed to CustomerModel.
You don't need to specify the property Customers in the individual binding expressions for the columns:
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Id}" Header="ID" />
<DataGridTextColumn Binding="{Binding FirstName}" Header="First Name" />
<DataGridTextColumn Binding="{Binding LastName}" Header="Last Name" />
<DataGridTextColumn Binding="{Binding Email}" Header="Email" />
</DataGrid.Columns>
I was having the same issue when I had the TextBlock Text Binding inside of a DataTemplate and I ended up having to do:
Text={Binding DataContext.SuccessTxt}
to get it to work properly. Try adding "DataContext." in front of the property and see if that works.
public Window()
{
this.DataContext = this;
InitializeComponent();
}
public string Name {get;set;}
//xaml
<TextBlock Text="{Binding Name}"/>
this.DataContext = this; before InitializeComponent();
(DataContext need available before load xaml in InitializeComponent())
Properties Name should be public and { get; }
(If private then wpf can't access)

IDataErrorInfo does not fire when binding to a complex object

I have a simple dialog that contains edit boxes such as this:
<TextBox Text="{Binding Path=EmailSettings.SmtpServer, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" />
The dialog uses a Model as its data context (to simplify the model example INotifyPropertyChanged has not been shown nor is the code that creates the model and
sets the dialog data context to the model instance):
class EmailSettingsModel : IDataErrorInfo
{
public EmailSettingsModel ()
{
EmailSettings = new EmailSettings();
}
public EmailSettings EmailSettings
{ get; set; }
string _error;
public string Error
{
get { return _error; }
set { _error = value; }
}
public string this[string propertyName]
{
get
{
string errorMessage = null;
if ( string.Compare( propertyName, "EmailSettings.SmtpServer" ) == 0 )
{
if ( !string.IsNullOrWhiteSpace( EmailSettings.SmtpServer ) )
errorMessage = "SMTP server is not valid";
}
Error = errorMessage;
}
}
}
The model contains a property that is a simple POCO class that has several properties on it.
class EmailSettings
{
public string SmtpServer
{ get; set; }
}
I could not get the IDataErrorInfo indexer to fire and spent hours looking. When I changed the binding on the text box to use a simple property:
<TextBox Text="{Binding Path=SmtpServer, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" />
on the Model as below the IDataErrorInfo indexer fired.
class EmailSettingsModel
{
public string SmtpServer
{ get; set; }
}
Was IDataErrorInfo not called because I used a compound property for the binding statement. I have used complex properties like this for normal data binding and they work but for this example IDataErrorInfo was not called.
IDataErrorInfo fires only at the level where implemented
For example if you have Binding Path looking like this "viewModel.property1.property2.property3" you will need to implement IDataErrorInfo inside the class of viewModel and inside the class of property1 and inside the class of property2. Property3 is a string.
So in order to make it work for you just implement IDataErrorInfo anywhere else.

How to do I send validation messages form model to view in the MVVM pattern?

I've got a small test WPF MVVM application working in which a view allows the user to change the first or last names of customers and the full name automatically changes, so communication is going from M-to-MV-to-V and back, everything is fully decoupled, so far so good.
But now as I look to how I will begin extending this to build large applications with the MVVM pattern, I find the decoupling to be an obstacle, namely:
how will I do validation messages, e.g. if back in the model in the LastName setter I add code that prevents names over 50 characters from being set, how can I send a messsage to the view telling it to display a message that the name was too long?
in complex applications I may have dozens of views on a screen at one time, yet I understand that in MVVM each view has one and only one ViewModel assigned to it to provide it with data and behavior, so how is it that the views can interact with each other, e.g. in the above validation example, what if back in the customer model we want to inform a particular "MessageAreaView" to display the message "Last name may only contain 50 characters.", how to we communicate that up the stack to that particular view?
CustomerHeaderView.xaml (View):
<UserControl x:Class="TestMvvm444.Views.CustomerHeaderView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<StackPanel HorizontalAlignment="Left">
<ItemsControl ItemsSource="{Binding Path=Customers}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal">
<TextBox
Text="{Binding Path=FirstName, Mode=TwoWay}"
Width="100"
Margin="3 5 3 5"/>
<TextBox
Text="{Binding Path=LastName, Mode=TwoWay}"
Width="100"
Margin="0 5 3 5"/>
<TextBlock
Text="{Binding Path=FullName, Mode=OneWay}"
Margin="0 5 3 5"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Customer.cs (Model):
using System.ComponentModel;
namespace TestMvvm444.Model
{
class Customer : INotifyPropertyChanged
{
public int ID { get; set; }
public int NumberOfContracts { get; set; }
private string firstName;
private string lastName;
public string FirstName
{
get { return firstName; }
set
{
if (firstName != value)
{
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName
{
get { return lastName; }
set
{
if (lastName != value)
{
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName
{
get { return firstName + " " + lastName; }
}
#region PropertChanged Block
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
#endregion
}
}
For validation, have your view model implement IDataErrorInfo. As for communication between views, don't be afraid to write UI services (eg. a message service that allows and view model to contribute messages that will be displayed somewhere in the UI). Or if there is a hard relationship between view models (eg. one view model owns another) then the owning view model can hold a reference to the child view model.
A really simple way to add validation messages is to use binding.
Add an notifable property to your view model that defines whether the validation message should be displayed or not:
private Boolean _itemValidatorDisplayed;
public Boolean ItemValidatorDisplayed
{
get { return _itemValidatorDisplayed; }
set
{
_itemValidatorDisplayed= value;
_OnPropertyChanged("ItemValidatorDisplayed");
}
}
Add a convertor class that converts bool to visibility:
using System;
using System.Windows;
namespace xxx
{
public class BoolToVisibilityConverter : IValueConverter
{
public bool Negate { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
bool val = System.Convert.ToBoolean(value);
if (!Negate)
{
return val ? Visibility.Visible : Visibility.Collapsed;
}
else
{
return val ? Visibility.Collapsed : Visibility.Visible;
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
Bind to the property from the view and apply the converter:
<UserControl x:Class="ViewClass"
...
>
<UserControl.Resources>
<contract:BoolToVisibilityConverter Negate="False"
x:Key="BoolToVisibilityConverter" />
</UserControl.Resources>
...
<TextBlock Visibility="{Binding Converter={StaticResource BoolToVisibilityConverter}, Path=ItemValidatorDisplayed}" />
...
</UserControl>
You'll need to be setting the ViewModel as the datacontext of the view:
namespace xxx
{
public partial class ViewClass: UserControl
{
public ViewClass()
{
InitializeComponent();
this.DataContext = new ViewClass_ViewModel();
}
}
}
Bingo - perfectly working validation being pushed to any view that cares to subscribe to this ViewModel / Property.
You will also be able to bind validation to the source object collection in SL3 :-)

WPF DataGrid - Combining TimeSeries w/MultiBinding, lose change-notification. Why?

I have a class that has two ObservableCollection< TimeValue >'s, where TimeValue is a custom DateTime/Value pairing with change-notification (via INotifyPropertyChanged). I call these Targets and Actuals.
When I bind these to a chart, everything works perfectly, and I get two LineSeries. If I bind one of them to a DataGrid, with a column for "Date" and a column for "Value", works perfectly again. I even get the TwoWay binding that I need.
However, I need to have a DataGrid that has a "Date" column, and a column each for Targets and Actuals. The problem is that I need to list ALL dates in a range, whereas some of these dates may not have corresponding values in Targets, Actuals, or both.
So, I decided I would do a MultiBinding that takes Targets and Actuals as input, and outputs a combined TimeSeriesC, with null values whenever either of the originals had no value.
It works, but does not respond to any changes in the underlying data.
This works fine (binding to one ObservableCollection):
<ctrls:DataGrid Grid.Row="1" Height="400" AutoGenerateColumns="False" CanUserDeleteRows="False" SelectionUnit="Cell">
<ctrls:DataGrid.ItemsSource>
<Binding Path="Targets"/>
<!--<MultiBinding Converter="{StaticResource TargetActualListConverter}">
<Binding Path="Targets"/>
<Binding Path="Actuals"/>
</MultiBinding>-->
</ctrls:DataGrid.ItemsSource>
<ctrls:DataGrid.Columns>
<ctrls:DataGridTextColumn Header="Date" Binding="{Binding Date,StringFormat={}{0:ddd, MMM d}}"/>
<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value}"/>
<!--<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value[0]}"/>
<ctrls:DataGridTextColumn Header="Actual" Binding="{Binding Value[1]}"/>-->
</ctrls:DataGrid.Columns>
This works, but only when first initialized. No response to change-notification:
<ctrls:DataGrid Grid.Row="1" Height="400" AutoGenerateColumns="False" CanUserDeleteRows="False" SelectionUnit="Cell">
<ctrls:DataGrid.ItemsSource>
<!--<Binding Path="Targets"/>-->
<MultiBinding Converter="{StaticResource TargetActualListConverter}">
<Binding Path="Targets"/>
<Binding Path="Actuals"/>
</MultiBinding>
</ctrls:DataGrid.ItemsSource>
<ctrls:DataGrid.Columns>
<ctrls:DataGridTextColumn Header="Date" Binding="{Binding Date,StringFormat={}{0:ddd, MMM d}}"/>
<!--<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value}"/>-->
<ctrls:DataGridTextColumn Header="Target" Binding="{Binding Value[0]}"/>
<ctrls:DataGridTextColumn Header="Actual" Binding="{Binding Value[1]}"/>
</ctrls:DataGrid.Columns>
And here is my IMultiValueConverter:
class TargetActualListConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
TimeSeries<double> Targets = values[0] as TimeSeries<double>;
TimeSeries<double> Actuals = values[1] as TimeSeries<double>;
DateTime[] range = TimeSeries<double>.GetDateRange(Targets, Actuals);//Get min and max Dates
int count = (range[1] - range[0]).Days;//total number of days
DateTime currDate = new DateTime();
TimeSeries<double?[]> combined = new TimeSeries<double?[]>();
for (int i = 0; i < count; i++)
{
currDate = range[0].AddDays(i);
double?[] vals = { Targets.Dates.Contains(currDate) ? (double?)Targets.GetValueByDate(currDate) : null, Actuals.Dates.Contains(currDate) ? (double?)Actuals.GetValueByDate(currDate) : null };
combined.Add(new TimeValue<double?[]>(currDate, vals));
}
return combined;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
TimeSeries<double?[]> combined = value as TimeSeries<double?[]>;
TimeSeries<double> Targets = new TimeSeries<double>();
TimeSeries<double> Actuals = new TimeSeries<double>();
foreach (TimeValue<double?[]> tv in combined)
{
if(tv.Value[0]!=null)
Targets.Add(new TimeValue<double>(tv.Date,(double)tv.Value[0]));
if (tv.Value[1] != null)
Actuals.Add(new TimeValue<double>(tv.Date, (double)tv.Value[1]));
}
TimeSeries<double>[] result = { Targets, Actuals };
return result;
}
}
I can't be too far off, since it displays the values.
What am I doing wrong?
Or, alternatively, is there an easier way of doing this?
Thanks all!
Looks like this is caused by the converter. ObservableCollection implements INotifyCollectionChanged, which notifies the UI when there is a change to the collection (Add/Remove/Replace/Move/Reset). These are all changes to the collection, not the contents of the collection, and so the updates you were seeing before were due to your class implementing INotifyPropertyChanged. Because the MultiCoverter is returning a new collection of new objects, the data in the initial collections will not propagate to these, since there's no bindings to the original objects for them to notify.
The first thing I would suggest is to take a look at the CompositeCollection Element and see if that will fit your needs.
Instead of setting the ItemsSource as you are, you could maintain the original objects with something like:
<ctrls:DataGrid.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Targets}" />
<CollectionContainer Collection="{Binding Actuals}" />
</CompositeCollection>
</ctrls:DataGrid.ItemsSource>
(I'm assuming 'does not respond to any changes in the underlying data' refers to changing the values, not modifying the collection, if I'm incorrect let me know and I'll take a deeper look at it.)
Edit Additions
In the case that that doesn't work an alternative is to write a new class that will wrap both the Target and Actual collections. Then a single ObservableCollection can be created using these wrappers. This is actually a better method over using a ValueConverter or using a CompositeCollection. With either you loose some of the functionality that was originally present. By using a value converter to recreate a collection, it no longer is bound directly to the original objects and so property notification may be lost. By using the CompositeCollection, you no longer have a single collection that can be iterated through or modified with add/delete/move etc, as it has to know which collection to operate upon.
This type of wrapping functionality can be quite useful in WPF, and is a very simplified version of a ViewModel, a part of the M-V-VM design pattern. It can be used when you don't have access to the underlying classes to add INotifyPropertyChanged or IDataErrorInfo, and can also help add additional functionality such as state and interaction to the underlying models.
Here's a short example demonstrating this functionality where both of our initial classes have the same Name property and don't implement INotifyPropertyChanged that is not shared between them.
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
Foo foo1 = new Foo { ID = 1, Name = "Foo1" };
Foo foo3 = new Foo { ID = 3, Name = "Foo3" };
Foo foo5 = new Foo { ID = 5, Name = "Foo5" };
Bar bar1 = new Bar { ID = 1, Name = "Bar1" };
Bar bar2 = new Bar { ID = 2, Name = "Bar2" };
Bar bar4 = new Bar { ID = 4, Name = "Bar4" };
ObservableCollection<FooBarViewModel> fooBar = new ObservableCollection<FooBarViewModel>();
fooBar.Add(new FooBarViewModel(foo1, bar1));
fooBar.Add(new FooBarViewModel(bar2));
fooBar.Add(new FooBarViewModel(foo3));
fooBar.Add(new FooBarViewModel(bar4));
fooBar.Add(new FooBarViewModel(foo5));
this.DataContext = fooBar;
}
}
public class Foo
{
public int ID { get; set; }
public string Name { get; set; }
}
public class Bar
{
public int ID { get; set; }
public string Name { get; set; }
}
public class FooBarViewModel : INotifyPropertyChanged
{
public Foo WrappedFoo { get; private set; }
public Bar WrappedBar { get; private set; }
public int ID
{
get
{
if (WrappedFoo != null)
{ return WrappedFoo.ID; }
else if (WrappedBar != null)
{ return WrappedBar.ID; }
else
{ return -1; }
}
set
{
if (WrappedFoo != null)
{ WrappedFoo.ID = value; }
if (WrappedBar != null)
{ WrappedBar.ID = value; }
this.NotifyPropertyChanged("ID");
}
}
public string BarName
{
get
{
return WrappedBar.Name;
}
set
{
WrappedBar.Name = value;
this.NotifyPropertyChanged("BarName");
}
}
public string FooName
{
get
{
return WrappedFoo.Name;
}
set
{
WrappedFoo.Name = value;
this.NotifyPropertyChanged("FooName");
}
}
public FooBarViewModel(Foo foo)
: this(foo, null) { }
public FooBarViewModel(Bar bar)
: this(null, bar) { }
public FooBarViewModel(Foo foo, Bar bar)
{
WrappedFoo = foo;
WrappedBar = bar;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
And then in the Window:
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}"/>
<GridViewColumn Header="Foo Name" DisplayMemberBinding="{Binding FooName}"/>
<GridViewColumn Header="Bar Name" DisplayMemberBinding="{Binding BarName}"/>
</GridView>
</ListView.View>
</ListView>

Resources