I have a IMultivalueConverter which updates the background color of a StackPanel when PropertyA or PropertyB is changed. These Controls are created dynamically.
Problem:
I have added two StackPanels and changed the PropertyA in the code when a button is clicked. This leads to a property changed event.
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
For the first stackpanel the background color is not updated, but for the second stackpanel this.PropertyChanged inturn calls my MultiValueConverter and background color is updated.
I am not able to understand why only one control is getting updated when both belong to same type and eventhandler is not null.
EDIT:
If I drag and drop a cell value from other control (DevExpress DataGrid) into the first stackpanel and then change the property, the background is not getting updated. It works fine until I drag and drop.
Update:
<StackPanel.Background>
<MultiBinding Converter="{StaticResource ResourceKey=BackgroundColorConverter}">
<Binding Path="PropertyA" UpdateSourceTrigger="PropertyChanged" />
<Binding Path="PropertyB" UpdateSourceTrigger="PropertyChanged" />
</MultiBinding>
</StackPanel.Background>
Update 2:
I have also tried using MultiDataTrigger instead of Converter, but couldn't solve the problem.
Unless i miss understood you, i don't see any complication in doing that,
<Window.Resources>
<app:BackgroundColorConverter x:Key="BackgroundColorConverter"/>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" >
<TextBox Text="{Binding PropertyA}" Width="200"/>
<TextBox Text="{Binding PropertyB}" Width="200"/>
</StackPanel>
<StackPanel Grid.Row="1" Margin="5">
<StackPanel.Background>
<MultiBinding Converter="{StaticResource ResourceKey=BackgroundColorConverter}">
<Binding Path="PropertyA" UpdateSourceTrigger="PropertyChanged" />
<Binding Path="PropertyB" UpdateSourceTrigger="PropertyChanged" />
</MultiBinding>
</StackPanel.Background>
</StackPanel>
<StackPanel Grid.Row="2" Margin="5">
<StackPanel.Background>
<MultiBinding Converter="{StaticResource ResourceKey=BackgroundColorConverter}">
<Binding Path="PropertyA" UpdateSourceTrigger="PropertyChanged" />
<Binding Path="PropertyB" UpdateSourceTrigger="PropertyChanged" />
</MultiBinding>
</StackPanel.Background>
</StackPanel>
</Grid>
the Converter :
public class BackgroundColorConverter:IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values==null)
{
return null;
}
return
new SolidColorBrush(Color.FromRgb(byte.Parse(values[0].ToString()), byte.Parse(values[1].ToString()),
50));
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
..And the Code behind
public partial class MainWindow : Window,INotifyPropertyChanged
{
private byte _propertyA ;
private byte _propertyB;
public byte PropertyA
{
get
{
return _propertyA;
}
set
{
if (_propertyA == value)
{
return;
}
_propertyA = value;
OnPropertyChanged();
}
}
public byte PropertyB
{
get
{
return _propertyB;
}
set
{
if (_propertyB == value)
{
return;
}
_propertyB = value;
OnPropertyChanged();
}
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
let me know if i did miss something
Reason:
When a value is dragged over the StackPanel, I am setting the BackgroundColor manually.
stackpanel.BackGround = new SolidColorBrush(Color.FromArgb(255,255,255,141));
Solution:
When I commented this line, the MultiValue converter is called and BackGround color is updated properly.
I created a property which changes according to DragEnter, DragOver and DragLeave events and then converter is called, I evaluate this value and set the Background color in the converter.
Related
how to make Validation work on ContentControl ?
There is a class that is responsible for storing a value with units of measure, it is displayed in the content control through a dataset, I would like to check for correctness when displaying and editing it, for example, units cannot have a value of 2, or the value should be less than 10,000. solve this with multibinding, the validation rule is not satisfied when editing values.
Xaml file:
<Window x:Class="DELETE1.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:DELETE1"
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
mc:Ignorable="d"
x:Name="m_win"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<DataTemplate DataType="{x:Type local:ValWithUnits}">
<Border>
<DockPanel>
<ComboBox DockPanel.Dock="Right" Width="60" IsEnabled="True" SelectedIndex="{Binding Unit}" ItemsSource="{Binding Src, ElementName=m_win}" />
<TextBox Text="{Binding Val, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" IsEnabled="True"/>
</DockPanel>
</Border>
</DataTemplate>
<local:MultiBindingConverter x:Key="MultiBindingConverter" />
</Window.Resources>
<Grid>
<ContentControl x:Name="m_contentControl">
<ContentControl.Content>
<MultiBinding Mode="TwoWay" NotifyOnValidationError="True"
UpdateSourceTrigger="PropertyChanged"
diag:PresentationTraceSources.TraceLevel="High"
Converter="{StaticResource MultiBindingConverter}" >
<Binding Path="Val" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" diag:PresentationTraceSources.TraceLevel="High"></Binding>
<Binding Path="Unit" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" diag:PresentationTraceSources.TraceLevel="High"></Binding>
<MultiBinding.ValidationRules>
<local:ContentControlValidationRule ValidatesOnTargetUpdated="True" x:Name="MValidationRule"/>
</MultiBinding.ValidationRules>
</MultiBinding>
</ContentControl.Content>
</ContentControl>
</Grid>
</Window>
And code file:
public class ValWithUnits:INotifyPropertyChanged
{
private double m_val;
private int m_unit;
public double Val
{
get => m_val;
set
{
m_val = value;
OnPropertyChanged(nameof(Val));
}
}
public int Unit
{
get => m_unit;
set
{
m_unit = value;
OnPropertyChanged(nameof(Unit));
}
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ValWithUnits MyValueWithUnits { get; set; } = new ValWithUnits();
public MainWindow()
{
InitializeComponent();
var a = new Binding("MyValueWithUnits");
a.Source = this;
a.ValidatesOnDataErrors = true;
a.ValidatesOnExceptions = true;
a.NotifyOnValidationError = true;
m_contentControl.SetBinding(ContentControl.ContentProperty, a);
}
public IEnumerable<int> Src => new int[] { 1,2,3};
}
public class ContentControlValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
//Debug.Fail("validate");
return ValidationResult.ValidResult;
}
private static ValidationResult BadValidation(string msg) =>
new ValidationResult(false, msg);
}
public class MultiBindingConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return string.Format("{0}-{1}", values[0], values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] { 1, 1 };
}
}
Validation 'ContentControlValidationRule' which I install through multi-binding does not work.
I have the following datagrid
<Window
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" mc:Ignorable="d" x:Class="VenusProductInfoQueryWPF.MainWindow"
Height="350" Width="646" WindowStyle="None" ResizeMode="NoResize" Topmost="False" MouseLeftButtonDown="Window_MouseLeftButtonDown" AllowsTransparency="True" WindowStartupLocation="CenterScreen" ShowInTaskbar="True">
<Window.Resources>
<DataTemplate x:Key="DataTemplate1">
<Button Tag="{Binding Name}" Content="Show" Click="LinkButton_Click"></Button>
</DataTemplate>
<DataTemplate x:Key="DataTemplate2">
<Button Tag="{Binding Sex}" Content="Show" Click="LinkButton_Click"></Button>
</DataTemplate>
</Window.Resources>`
<Grid>
<DataGrid x:Name="MyDataGrid" HorizontalAlignment="Left" Margin="60,44,0,0"
VerticalAlignment="Top" Height="223" Width="402" AutoGenerateColumns="False"
AutoGeneratedColumns="MyDataGrid_AutoGeneratedColumns">
<DataGrid.Columns>
<DataGridTextColumn Header="Age" Binding="{Binding Path=Age}"></DataGridTextColumn>
<DataGridTemplateColumn Header="Sex" CellTemplate="{StaticResource DataTemplate2}"/>
<DataGridTemplateColumn Header="Name" CellTemplate="{StaticResource DataTemplate1}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
and in the codebehind I have:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Media;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
dataSource = new DataTable();
dataSource.Columns.Add("Age");
dataSource.Columns.Add("Name");
dataSource.Columns.Add("Sex");
AddNewRow(new object[] { 10, "wang", "Male" });
AddNewRow(new object[] { 15, "huang", "Male" });
AddNewRow(new object[] { 20, "gao", "Female" });
dataGrid1.ItemsSource = dataSource.AsDataView();
}
}
}
The datatable has more than 30 columns (I only wrote 2 in here to make it easier to follow).. the question is: If I want to show the same template style with different binging source in every column, do I really have to define many different datatemplates (like DataTemplate1, DataTemplate2, ... see above) to bind the CellTemplate of each DataGridTemplateColumn to it ? Can I define one datatemplate and in the code or through other way to dynamic set the binding? Thank you for your answer!
There is a way but it is not pretty,
For brevity I am using the code behind as the view model and I have removed exception handling and unnecessary lines.
I have convert your object array into a Person object (for reasons that should become evident later in the solution)
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Sex { get; set; }
}
I have converted your DataSource into an ObservableCollection So the code behind is now
public partial class MainWindow : Window
{
public MainWindow()
{
Items = new ObservableCollection<Person>()
{
new Person {Age = 10, Name = "wang", Sex="Male"},
new Person {Age = 15, Name = "huang", Sex="Male"},
new Person {Age = 20, Name = "gao", Sex="Female"}
};
ShowCommand = new DelegateCommand(ExecuteShowCommand, CanExecuteShowCommand);
InitializeComponent();
}
private bool CanExecuteShowCommand(object arg) { return true; }
private void ExecuteShowCommand(object obj) { MessageBox.Show(obj != null ? obj.ToString() : "No Parameter received"); }
public DelegateCommand ShowCommand { get; set; }
public ObservableCollection<Person> Items { get; set; }
}
The DelegateCommand is defined as
public class DelegateCommand : ICommand
{
private Func<object, bool> _canExecute;
private Action<object> _execute;
public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
{
_canExecute = canExecute;
_execute = execute;
}
public bool CanExecute(object parameter) { return _canExecute.Invoke(parameter); }
void ICommand.Execute(object parameter) { _execute.Invoke(parameter); }
public event EventHandler CanExecuteChanged;
}
This allows the use of Commands and CommandParameters in the single template.
We then use a MultiValueConverter to get the data for each button. It uses the header of the column to interrogate, using reflection, the Person object for the required value and then pass it back as the parameter of the command.
public class GridCellToValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
object returnValue = null;
var person = values.First() as Person;
var propertyName = values[1] == DependencyProperty.UnsetValue ? string.Empty : (string)values[1];
if ((person != null) && (!string.IsNullOrWhiteSpace(propertyName))) { returnValue = person.GetType().GetProperty(propertyName).GetValue(person); }
return returnValue;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); }
}
The final piece of the puzzle is the xaml
<Window x:Class="StackOverflow.Q26731995.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:q26731995="clr-namespace:StackOverflow.Q26731995" Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<q26731995:GridCellToValueConverter x:Key="GridCell2Value" />
<DataTemplate x:Key="ButtonColumnDataTemplate">
<Button Content="Show" Command="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=ShowCommand}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource GridCell2Value}">
<Binding /> <!-- The person object -->
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGridCell}}" Path="Column.Header" /> <!-- The name of the field -->
</MultiBinding>
</Button.CommandParameter>
</Button>
</DataTemplate>
</Window.Resources>
<Grid>
<DataGrid x:Name="MyDataGrid" HorizontalAlignment="Left" Margin="60,44,0,0" ItemsSource="{Binding Path=Items}" VerticalAlignment="Top" Height="223" Width="402" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Age" Binding="{Binding Path=Age}"></DataGridTextColumn>
<DataGridTemplateColumn Header="Sex" CellTemplate="{StaticResource ButtonColumnDataTemplate}"/>
<DataGridTemplateColumn Header="Name" CellTemplate="{StaticResource ButtonColumnDataTemplate}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
You should be able to cut and past this code into a new wpf app and see it work. Not I have not take care of the AddNewItem row the grid adds (because we have not turned it off).
This can be done with a collection of object array rather than a collection of Person. To do so, you would need to pass the DataGridCellsPanel into the converter and use it with the header to calculate the index of the required value. The convert would look like
<MultiBinding Converter="{StaticResource GridCell2Value}">
<Binding /> <!-- The data -->
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGridCell}}" Path="Column.Header}" /> <!-- The name of the field -->
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGridCellsPanel}}" /> <!-- The panel that contains the row -->
</MultiBinding>
The converter code would be along the lines
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
object returnValue = null;
var data = values.First() as object[];
var property = values[1] == DependencyProperty.UnsetValue ? string.Empty : (string)values[1];
var panel = values[2] as DataGridCellsPanel;
var column = panel.Children.OfType<DataGridCell>().FirstOrDefault(c => c.Column.Header.Equals(property));
if (column != null)
{
returnValue = data[panel.Children.IndexOf(column)];
}
return returnValue;
}
I hope this helps.
I have textbox A on tab A and textbox B on tab B. I databind them to the same property. I use the same binding:
<TextBox>
<TextBox.Text>
<Binding Path=ValueA UpdateSourceTrigger="LostFocus">
<Binding.ValidationRules>
<local:MyValidationRules />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
But there is a problems. if the value in textbox A is invalidate, it will not update the property in the source. But it will looks find if the user switches to tab b. Basically, the validation rule is blocking the updates from changing the source.
I tried to bind textbox b to textbox a. The problem was the validation rules seem to work for only textbox a, but not b.
Design wise, if the user entered an invalid value in textbox A, you do not want to show it on textbox B, since it's invalid. If I were you I would deny the user access to tab B until the textbox A is valid
Here is an example :
View:
<Grid>
<Grid.Resources>
<local:TabSelectionConverter x:Key="tabSelectionConverter"/>
</Grid.Resources>
<TabControl x:Name="myTabControl">
<TabItem x:Name="TabA" Header="Tab A">
<TextBox x:Name="TextBoxA" Text="{Binding MyTextProperty}" Height="20"/>
</TabItem>
<TabItem x:Name="TabB" Header="Tab B">
<TextBox x:Name="TextBoxB" Text="{Binding MyTextProperty}" Height="20"/>
</TabItem>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction Command="{Binding TabSelectionChangedCommand}">
<i:InvokeCommandAction.CommandParameter>
<MultiBinding Converter="{StaticResource tabSelectionConverter}">
<Binding ElementName="myTabControl"/>
<Binding ElementName="TextBoxA" Path="Text"/>
</MultiBinding>
</i:InvokeCommandAction.CommandParameter>
</i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</TabControl>
</Grid>
Converter that will create an object with all the neccassary elements inroder to validate an deny access to second tab:
public class TabSelectionConverter : IMultiValueConverter
{
#region IMultiValueConverter Members
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new TabSelectionParameters
{
MainTab = values[0] as TabControl,
InputText = values[1] as string
};
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
public class TabSelectionParameters
{
public TabControl MainTab { get; set; }
public string InputText { get; set; }
}
And your logic in the ViewModel:
public class MainWindowViewModel : NotificationObject
{
private string myTextProperty;
public MainWindowViewModel()
{
TabSelectionChangedCommand = new DelegateCommand<TabSelectionParameters>(parameters =>
{
if (parameters.InputText == string.Empty) // Here goes the validation of the user input to TextBoxA
{
// Deny selection of tab B by returning the selection index to Tab A
parameters.MainTab.SelectedIndex = 0;
}
});
}
public DelegateCommand<TabSelectionParameters> TabSelectionChangedCommand { get; set; }
public string MyTextProperty
{
get
{
return myTextProperty;
}
set
{
myTextProperty = value;
RaisePropertyChanged(() => MyTextProperty);
}
}
}
Hope this helps
I have a DataGrid within a UserControl. I want to have a DataGridTextColumn bound to a DateTime field, showing only the time. When the user enters a time, the date portion (year, month, day) should be taken from a property (AttendDate) on the UserControl.
My first thought was to bind the user control's property to ConverterParameter:
<DataGridTextColumn Header="From"
Binding="{Binding FromDate, Converter={StaticResource TimeConverter},ConverterParameter={Binding AttendDate,ElementName=UC}}"
/>
but ConverterParameter doesn't take a binding. I then thought to do this using a MultiBinding:
<DataGridTextColumn Header="משעה" Binding="{Binding FromDate, Converter={StaticResource TimeConverter}}" />
<DataGridTextColumn.Binding>
<MultiBinding Converter="{StaticResource TimeConverter}">
<Binding Path="FromDate" />
<Binding Path="AttendDate" ElementName="UC" />
</MultiBinding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
However IMultiValueConverter.Convert -- which takes multiple parameters -- is only called when formatting the display. IMultiValueConverter.ConvertBack which is called on editing, only takes one parameter - the entered string.
How can I do this?
(I am not using MVVM; not something I can change.)
One idea for the solution is to have another property with only a getter that merges the info you want.
something like
property string Time {get {return this.FromDate.toshortdate().tostring() + AttendDate.hour.tostring() + attenddate.minutes.tostring()}; }
The code might be not exactly this, but then you can bind this property to show the info you want where it should be presented.
regards,
=============EDIT===========
I tried this, a very simple example...don't know if it works for you:
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv ="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<conv:TimeConverter x:Key="tmeConverter"></conv:TimeConverter>
</Window.Resources>
<Grid>
<DataGrid ItemsSource="{Binding listExample}">
<DataGrid.Columns>
<DataGridTextColumn Header="teste" >
<DataGridTextColumn.Binding>
<MultiBinding Converter="{StaticResource tmeConverter}">
<Binding Path="FromDate" />
<Binding Path="AttendDate" />
</MultiBinding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Code behind (.cs)
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public List<Example2> listExample { get; set; }
public Example2 Test { get; set; }
public MainWindow()
{
InitializeComponent();
this.listExample = new List<Example2>();
//listExample.Add(new Example { IsChecked = false, Test1 = "teste" });
//listExample.Add(new Example { IsChecked = false, Test1 = "TTTTT!" });
this.Test = new Example2 { AttendDate = "1ui", FromDate = "ff" };
this.listExample.Add(this.Test);
DataContext = this;
}
}
}
And Example2 class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApplication1
{
public class Example2
{
public string FromDate { get; set; }
public string AttendDate { get; set; }
}
}
Converter:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace WpfApplication1
{
class TimeConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return values[0].ToString() + values[1].ToString();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
The final window appears 3 columns, and if I change the last two columns, the first one is automatically edited.
Is that it?
Get rid of DataGridTextColumn and use DataGridTemplateColumn with CellTemplate containing a textblock bound to your multibinding, and cell editing template containing TextBox bound to FromDate, possibly via short-date converter, depending on usability you intend to achieve.
On of possible solutions:
XAML
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label>From time</Label>
<DatePicker SelectedDate="{Binding FromTime}"/>
</StackPanel>
<DataGrid ItemsSource="{Binding AllUsers}" AutoGenerateColumns="False">
<DataGrid.Resources>
<local:TimeConverter x:Key="TimeConverter"/>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:UserViewModel">
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource TimeConverter}">
<Binding ElementName="root" Path="DataContext.FromTime"/>
<Binding Path="AttendTimeHour"/>
<Binding Path="AttendTimeMinute"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate DataType="local:UserViewModel">
<StackPanel Orientation="Horizontal">
<TextBlock>
<Run>Attend on </Run>
<Run Text="{Binding ElementName=root, Path=DataContext.FromTime, StringFormat=d}"/>
<Run> at </Run>
</TextBlock>
<TextBox Text="{Binding AttendTimeHour}"/><TextBlock>:</TextBlock>
<TextBox Text="{Binding AttendTimeMinute}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
View model (UserViewModel) part:
private DateTime _attendTime;
public DateTime AttendTime
{
get { return _attendTime; }
set
{
if (value == _attendTime) return;
_attendTime = value;
OnPropertyChanged();
OnPropertyChanged("AttendTimeHour");
OnPropertyChanged("AttendTimeMinute");
}
}
public int AttendTimeHour
{
get { return attendTimeHour; }
set
{
if (value.Equals(attendTimeHour)) return;
attendTimeHour = value;
AttendTime = new DateTime(AttendTime.Year, AttendTime.Month, AttendTime.Day, AttendTimeHour, AttendTime.Minute, AttendTime.Second);
OnPropertyChanged();
}
}
private int _attendTimeMinute;
public int AttendTimeMinute
{
get { return _attendTimeMinute; }
set
{
if (value == _attendTimeMinute) return;
_attendTimeMinute = value;
AttendTime = new DateTime(AttendTime.Year, AttendTime.Month, AttendTime.Day, AttendTime.Hour, AttendTimeMinute, AttendTime.Second);
OnPropertyChanged();
}
}
And the converter
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 2) return null;
if (!(values[0] is DateTime && values[1] is int && values[2] is int)) return null;
var fromDate = (DateTime) values[0];
var attendTimeHour = (int) values[1];
var attendTimeMinutes = (int)values[2];
var result = new DateTime(fromDate.Year, fromDate.Month, fromDate.Day, attendTimeHour, attendTimeMinutes, 0);
return result.ToString();
}
I have successfully applied the trick explained here. But I still have one problem.
Quick recap : I display users in a ListView. Users are regrouped by Country, and in the GroupStyle DataTemplate I display the sum of all group related Users.Total, using a Converter. But UI users can change the "Total" property value of Users through a modal window.
When there is only one item in the Group, both the User Total displayed and the sum are properly updated. But when there are multiple items in the group, only the User Total is updated (through binding) but the Converter that's supposed to make the sum (TotalSumConverter) is not even called!
Do you have any idea where it could come from? Should I use some kind of a trigger to make sure the Converter is called when there is a modification in the items?
The problem is that the value converter that calculates the sum for all the items in a group don't run when an item is changed, since there's no notification for changed items. One solution is to bind to something else that you can control how it does notifications and notify the group header when needed.
Below is a working example. You can change the count for a user in the text box and totals gets recalculated.
XAML:
<Window x:Class="UserTotalTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:userTotalTest="clr-namespace:UserTotalTest"
Title="Window1" Height="300" Width="300"
Name="this">
<Window.Resources>
<userTotalTest:SumConverter x:Key="SumConverter" />
<CollectionViewSource Source="{Binding Path=Users}" x:Key="cvs">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Country"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="10" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListView
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding Source={StaticResource cvs}}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Path=Name}" />
<GridViewColumn Header="Count" DisplayMemberBinding="{Binding Path=Count}" />
</GridView.Columns>
</GridView>
</ListView.View>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<StackPanel Margin="10">
<TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />
<ItemsPresenter />
<TextBlock FontWeight="Bold">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource SumConverter}">
<MultiBinding.Bindings>
<Binding Path="DataContext.Users" ElementName="this" />
<Binding Path="Name" />
</MultiBinding.Bindings>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
<ComboBox Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2"
ItemsSource="{Binding Path=Users}"
DisplayMemberPath="Name"
SelectedItem="{Binding Path=SelectedUser}" />
<TextBlock Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding Path=SelectedUser.Country}" />
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding Path=SelectedUser.Count}" />
</Grid>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Data;
namespace UserTotalTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new UsersVM();
}
}
public class UsersVM : INotifyPropertyChanged
{
public UsersVM()
{
Users = new List<User>();
Countries = new string[] { "Sweden", "Norway", "Denmark" };
Random random = new Random();
for (int i = 0; i < 25; i++)
{
Users.Add(new User(string.Format("User{0}", i), Countries[random.Next(3)], random.Next(1000)));
}
foreach (User user in Users)
{
user.PropertyChanged += OnUserPropertyChanged;
}
SelectedUser = Users.First();
}
private void OnUserPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Count")
{
PropertyChanged(this, new PropertyChangedEventArgs("Users"));
}
}
public List<User> Users { get; private set; }
private User _selectedUser;
public User SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value; if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("SelectedUser"));
}
}
}
public string[] Countries { get; private set; }
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class User : INotifyPropertyChanged
{
public User(string name, string country, double total)
{
Name = name;
Country = country;
Count = total;
}
public string Name { get; private set; }
private string _country;
public string Country
{
get { return _country; }
set
{
_country = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Country"));
}
}
}
private double _count;
public double Count
{
get { return _count; }
set
{
_count = value; if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Count"));
}
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class SumConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
IEnumerable<User> users = values[0] as IEnumerable<User>;
string country = values[1] as string;
double sum = users.Cast<User>().Where(u =>u.Country == country).Sum(u => u.Count);
return "Count: " + sum;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
The trick you are using databinds the group footer to ListView.Items which will not update your view automatically, like a DependencyObject does for example. Instead force a refresh after each update to Total like this:
CollectionViewSource viewSource = FindResource("ViewSource") as CollectionViewSource;
viewSource.View.Refresh();
The problem with refreshing the view is that it completely refreshes it, and I have expanders for my grouping, that will expand back to their original state, even if the user closed them. So yes it's a possible workaround, but it's not completely satisfying.
Also your DependencyObject explanation is interesting, but then, why is it working when I have only one item in my group?