Bind ItemsControl with ItemsSource to the array index - wpf

I want to bind an ItemsControl to an array of object, using ItemsSource and DataTemplate.
And I want to show the index of each item.
Like
Customer 1:
Name: xxxx
Age:888
Customer 2:
Name: yyy
Age: 7777

The simplest way to do this is to add an Index property to you class ;-) Otherwise it can be done using a MultiValueConverter:
<Window x:Class="IndexSpike.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
xmlns:Converters="clr-namespace:IndexSpike"
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
Name="Root"
>
<Window.Resources>
<Converters:GetIndex x:Key="GetIndexConverter"/>
</Window.Resources>
<StackPanel>
<ItemsControl ItemsSource="{Binding Persons}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Margin="0,5,0,0">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource
GetIndexConverter}"
StringFormat="Index: {0}">
<Binding Path="."/>
<Binding ElementName="Root" Path="Persons"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock Text="{Binding Name, StringFormat='Name: {0}'}"/>
<TextBlock Text="{Binding Age, StringFormat='Age {0}'}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Window>
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace IndexSpike
{
public partial class Window1 : Window
{
public ObservableCollection<Person> Persons { get; set; }
public Window1()
{
Persons=new ObservableCollection<Person>
{
new Person("Me",20),
new Person("You",30)
};
InitializeComponent();
DataContext = this;
}
}
public class Person
{
public Person(string name,int age)
{
Name = name;
Age=age;
}
public string Name { get; set; }
public int Age { get; set; }
}
public class GetIndex:IMultiValueConverter
{
#region Implementation of IMultiValueConverter
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var persons = (ObservableCollection<Person>) values[1];
var person = (Person) values[0];
return persons.IndexOf(person);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
Thanks to this question I found an other way to declare the MultiBinding:
<MultiBinding Converter="{StaticResource GetIndexConverter}"
StringFormat="Index: {0}">
<Binding Path="."/>
<Binding RelativeSource="{RelativeSource FindAncestor,
AncestorType={x:Type ItemsControl}}"
Path="DataContext.Persons"/>
</MultiBinding>

Related

Why the width of column in the datagrid is not set with the multivalue converter?

I want to set the width of a column with a multivalue converter, but the width is not set.
Then converter, for testing, always return 500. The xaml code is this:
<DataGridTextColumn Header="Modelo"
Binding="{Binding Modelo}"
HeaderStyle="{StaticResource DataGridColumnHeaderLeftAlignement}">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell" BasedOn="{StaticResource ResourceKey=DataGridCellLeftHorizontalAlignment}">
<Setter Property="Width">
<Setter.Value>
<MultiBinding Converter="{StaticResource docListadosDocumentosDataGridComponentesColumnWidthMultiValueConverter}">
<MultiBinding.Bindings>
<Binding />
</MultiBinding.Bindings>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
I am trying to set the width of the cell. Perhaps should I set up the width of of another element of the visual tree?
Thanks.
EDIT: a simple example
The view model:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
using System.Runtime.CompilerServices;
using System.Globalization;
namespace ModificarColumnaDataGridConMultivalueConverter
{
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
ObservableCollection<Data> col = new ObservableCollection<Data>();
col.Add(new Data() { SomeString = 44 });
col.Add(new Data() { SomeString = 84 });
col.Add(new Data() { SomeString = 104 });
cvs.Source = col;
}
private CollectionViewSource cvs = new CollectionViewSource();
public ICollectionView View { get => cvs.View; }
private double _widthValue = 30;
public double WidthValue
{
get => this._widthValue;
set { this._widthValue = value; OnPorpertyChanged(); }
}
private bool _widthDefault = false;
public bool WidthDefault
{
get => this._widthDefault;
set { this._widthDefault = value; OnPorpertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPorpertyChanged([CallerMemberName] string propName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
public class Data
{
public double SomeString { get; set; }
}
public class MyMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return (double)200;
double w = 30;
if (!(bool)values[1]) double.TryParse(values[0].ToString(), out w);
return w;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
The view:
<Window x:Class="ModificarColumnaDataGridConMultivalueConverter.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:ModificarColumnaDataGridConMultivalueConverter"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:ViewModel x:Key="vm"/>
<local:MyMultiValueConverter x:Key="MyMultiValueConverter"/>
</Window.Resources>
<StackPanel DataContext="{StaticResource vm}">
<DataGrid Name="dg" ItemsSource="{Binding View}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Col1" Binding="{Binding SomeString}" MinWidth="0" MaxWidth="300" >
<DataGridTextColumn.Width>
<MultiBinding Converter="{StaticResource MyMultiValueConverter}">
<Binding Path="WidthValue" Source="{StaticResource vm}"/>
<Binding Path="WidthDefault" Source="{StaticResource vm}"/>
</MultiBinding>
</DataGridTextColumn.Width>
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="Width">
<Setter.Value>
<MultiBinding Converter="{StaticResource MyMultiValueConverter}">
<Binding Path="WidthValue" Source="{StaticResource vm}"/>
<Binding Path="WidthDefault" Source="{StaticResource vm}"/>
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<CheckBox IsChecked="{Binding WidthDefault}" Content="Default Width"/>
<Slider Width="200" Height="20" Minimum="5" Maximum="300" Value="{Binding WidthValue}"/>
<!--<TextBox Text="{Binding WidthValue}"/>-->
</StackPanel>
</Window>
The problem with this solution is that with the slide, I can encrease the width of the column, but I can't decrease.
Another problem is if in the converter I return just a value, for example 500, it is not setted.
NOTE: I have realised that if I cast to double the value it works.
But the problem is that I can increase the width with the slide, but not decrease.
The converter should return a DataGridLength for the width of the column to be set:
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double w = 30;
if (!(bool)values[1]) double.TryParse(values[0].ToString(), out w);
return new DataGridLength(w);
}
This means that you cannot use the same converter to set the Width of the column and the TextBlock since the latter expects a double.

Set ContentPresenter ContentTemplate on value change in generated field

I have am attempting to build a tree view where:
1. The TreeViewItems are generated by a list in my model.
2. Each TreeViewItem contains a ComboBox, and a dynamic element whose template I want to change based on the value selected in the ComboBox.
Here is my current xaml code.
<Window x:Class="MyTestWPF.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:MyTestWPF"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:NodeTypeToTemplateConverter x:Key="NodeTypeToTemplateConverter"/>
<DataTemplate x:Key="Template1">
<TextBlock Text="Template 1" />
</DataTemplate>
<DataTemplate x:Key="Template2">
<TextBlock Text="Template 2" />
</DataTemplate>
<Style x:Key="MyNodeTemplate" TargetType="ContentPresenter">
<Setter Property="ContentTemplate" Value="{StaticResource Template1}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=NodeType}">
<DataTrigger.Value>
<local:NodeTypesEnum>Type1</local:NodeTypesEnum>
</DataTrigger.Value>
<Setter Property="ContentTemplate" Value="{Binding Converter={StaticResource NodeTypeToTemplateConverter}}"/>
</DataTrigger>
</Style.Triggers>
</Style>
<HierarchicalDataTemplate DataType="{x:Type local:MyTreeNode}"
ItemsSource="{Binding Nodes}">
<StackPanel Orientation="Horizontal">
<ComboBox ItemsSource="{Binding Path=GetAvailableNodeType}"
SelectedItem="{Binding Path=NodeType}" />
<ContentPresenter Style="{StaticResource MyNodeTemplate}" Content="{Binding}" />
</StackPanel>
</HierarchicalDataTemplate>
</Window.Resources>
<TreeView x:Name="MyTree" ItemsSource="{Binding MyTreeModel}" />
</Window>
And its code-behind:
using System.Windows;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new
{
MyTreeModel = new MyTreeNode[] {
new MyTreeNode() { Name = "1", Nodes = new MyTreeNode[] { new MyTreeNode() { Name= "2" } } }
}
};
}
}
The tree node type:
namespace MyTestWPF
{
public class MyTreeNode
{
public string Name { get; set; }
public NodeTypesEnum NodeType { get; set; }
public MyTreeNode[] Nodes { get; set; }
public NodeTypesEnum[] GetAvailableNodeType()
{
return new NodeTypesEnum[] { NodeTypesEnum.Type1, NodeTypesEnum.Type2 };
}
}
public enum NodeTypesEnum
{
Type1 = 0,
Type2 = 1
}
}
The Converter (NodeTypeToTemplateConverter) receives the whole ViewModel, and returns the name of the relevant template based on values in the model.
using System;
using System.Globalization;
using System.Windows.Data;
namespace MyTestWPF
{
public class NodeTypeToTemplateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if((value as MyTreeNode).NodeType == NodeTypesEnum.Type1)
{
return "Template1";
} else
{
return "Template2";
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
The problem is that the above code causes a stack overflow exception. The first item in the TreeView endlessly calls NodeTypeToTemplateConverter's Convert method.
I figured it had to do with the DataTrigger.Value. Setting that to a value different from the default NodeType allows the page to load without overflow, but the moment any ComboBox is set to NodeType1, stack overflow.
I attempted to simply remove the DataTrigger.Value element, but that causes the Converter to never be called at all...
How can I dynamically build the template name based on the value selected by its neighboring ComboBox?
You probably want to use a DataTemplateSelector rather than a converter.
public class ComboBoxItemTemplateSelector : DataTemplateSelector
{
public DataTemplate Template1 { get; set; }
public DataTemplate Template2 { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
//Logic to select template based on 'item' value.
if (item == <template1Value>) return Template1; //TODO: replace <template1Value>
else if (item == <template2Value>) return Template2; //TODO: replace <template2Value>
else return new DataTemplate();
}
}
<local:ComboBoxItemTemplateSelector x:Key="ComboBoxItemTemplateSelector">
<local:ComboBoxItemTemplateSelector.Template1>
<DataTemplate>
<TextBlock Text="" />
</DataTemplate>
</local:ComboBoxItemTemplateSelector.Template1>
<local:ComboBoxItemTemplateSelector.Template2>
<DataTemplate>
<TextBlock Text="" />
</DataTemplate>
</local:ComboBoxItemTemplateSelector.Template2>
</local:ComboBoxItemTemplateSelector>
<ContentPresenter Content="{Binding NodeType}" ContentTemplateSelector="{StaticResource ComboBoxItemTemplateSelector}"/>
I have not fully tested this code, so let me know if you have any issues.
EDIT:
The template selector is only executed when the content changes so this won't work if you use {Binding}. A workaround for this would be to have the DataTemplate content bind to the parent's DataContext.
<DataTemplate>
<TextBlock Text="" DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=ContentPresenter}}"/>
</DataTemplate>
If this workaround is not acceptable, there are other ways to do this as well.

WPF use same datatemplate with different binding

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.

How to bind class with Dictionary<string,object> property to datagrid

I have a scenario where i have to dynamically fetch the data and store it in a dictionary for display.
This is how my Model looks like:
public class ViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public Dictionary<string,object> CustomFields { get; set; }
}
I am binding my view model to DataGrid and i have two columns defined for Id and Name.
How do i generate columns for the data in the dictionary?
I cannot use Dynamic object as i am using .net 3.5.
This is how my XAML looks like:
<Window x:Class="MyClass"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:tk="http://schemas.microsoft.com/wpf/2008/toolkit"
xmlns:viewmodel="clr-namespace:MyViewModel"
xmlns:model="clr-namespace:MyModel"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="List Of Projects" Height="600" Width="600">
<Window.DataContext>
<viewmodel:SomeViewModel></viewmodel:SomeViewModel>
</Window.DataContext>
<tk:DataGrid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" x:Name="dgData" ItemsSource="{Binding ListOfObjects}" VerticalContentAlignment="Stretch" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" AutoGenerateColumns="False" SelectionMode="Single" CanUserAddRows="False" CanUserDeleteRows="False" >
<tk:DataGrid.Columns>
<tk:DataGridTextColumn Header="Id" Binding="{Binding Id}" IsReadOnly="True"></tk:DataGridTextColumn>
<tk:DataGridTextColumn Header="Name" Binding="{Binding Name}" IsReadOnly="True"></tk:DataGridTextColumn>
</tk:DataGrid.Columns>
</tk:DataGrid>
</Window>
Use a Converter :
CS :
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
public Dictionary<int, string> MyDictionary
{
get
{
var dic = new Dictionary<int, string>();
dic.Add(1,"11111");
dic.Add(2,"22222");
dic.Add(3,"33333");
dic.Add(4,"44444");
return dic;
}
}
}
public class DictionaryConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DataGridRow row = (DataGridRow)values[0];
IDictionary<int,string> dic = (IDictionary<int,string>)values[1];
bool isId = bool.Parse(parameter.ToString());
var index = row.GetIndex();
KeyValuePair<int,string> kv = dic.ElementAt(index);
return isId ? kv.Key.ToString() : kv.Value;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML :
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MyDictionary}" CanUserAddRows="False">
<DataGrid.Resources>
<loc:DictionaryConverter x:Key="dicConverter" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="ID">
<DataGridTextColumn.Binding>
<MultiBinding Converter="{StaticResource dicConverter}" ConverterParameter="True">
<Binding Path="." RelativeSource="{RelativeSource AncestorType=DataGridRow}" />
<Binding Path="ItemsSource" RelativeSource="{RelativeSource AncestorType=DataGrid}" />
</MultiBinding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
<DataGridTextColumn Header="Name">
<DataGridTextColumn.Binding>
<MultiBinding Converter="{StaticResource dicConverter}" ConverterParameter="False">
<Binding Path="." RelativeSource="{RelativeSource AncestorType=DataGridRow}" />
<Binding Path="ItemsSource" RelativeSource="{RelativeSource AncestorType=DataGrid}" />
</MultiBinding>
</DataGridTextColumn.Binding>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>

DataGridTextColumn showing/editing time only, date bound to property of parent usercontrol

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();
}

Resources