Setting DataGridTextColumn Width - wpf

I have a MVVM WPF application.
I have a DataGridTextColumn in a WPF datagrid. I want to bind its width property to a converter and pass to it its cell value. For this column, there are cases where all cells for this column are empty so I also want to set the column width to a fixed value, 20 (the same as its MinWidth) in case all cells are empty, otherwise 50. The problem is that converter is not being called.
To simplify and focus on the interesting parts I only post here the relevant code:
<DataGrid Grid.Row="1"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=MyListOfItems}"
VerticalAlignment="Stretch" IsReadOnly="True"
SelectionMode="Single" ColumnWidth="*"
>
<DataGridTextColumn
CellStyle="{StaticResource MyDataGridCellStyle}"
Binding="{Binding Path=EntryDate, StringFormat=\{0:dd/MM/yyyy\}}"
Header="Entry Date"
Width="{Binding Path=EntryDate, Converter={StaticResource ColumnWidthConverter}}"
HeaderStyle="{DynamicResource CenterGridHeaderStyle}">
</DataGridTextColumn>
</DataGrid>
Converter:
public class ColumnWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string cellContent= (string)value;
return (string.IsNullOrEmpty(cellContent.Trim()) ? 20 : 50);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
My final goal is to set column width to 20 when all its cells are empty, otherwise set its width to 50. I thought that using a converter it will be a good idea but converter is never called. Why?
UPDATE:
Finllay I have done what #Andy suggests: bind a property from view model to datagridtextcolumn width property on view. This property on view model iterates over all column cells, and then set the width accordingly. See below. My problem is that this property 'EntryDateColumnWidth' on view model only fires first time when application is launched, then when calling OnPropertyChanged("EntryDateColumnWidth"), it is not raised.
View model:
public class MyMainViewModel : ViewModelBase
{
public DataGridLength EntryDateColumnWidth
{
get
{
bool isEmpty = this.MyListOfItems.TrueForAll(i => string.IsNullOrEmpty(i.EntryDate.ToString().Trim()));
return (isEmpty ? 20 : new DataGridLength(0, DataGridLengthUnitType.Auto));
}
}
}
Also, from view model, when I have set the list of items for the datagrid, I perform:
OnPropertyChanged("EntryDateColumnWidth");
This property returns a DataGridLength object because I need to set width to auto when any of the column cells is not empty.
Note: ViewModelBase is an abstract class that implements INotifyPropertyChanged.
View:
<DataGrid Grid.Row="1"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=MyListOfItems}"
VerticalAlignment="Stretch" IsReadOnly="True"
SelectionMode="Single" ColumnWidth="*">
<DataGrid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>
<DataGridTextColumn
CellStyle="{StaticResource MyDataGridCellStyle}"
Binding="{Binding Path=EntryDate, StringFormat=\{0:dd/MM/yyyy\}}"
Header="Entry Date"
Width="{Binding Data.EntryDateColumnWidth, Source={StaticResource proxy}}"
HeaderStyle="{DynamicResource CenterGridHeaderStyle}">
</DataGridTextColumn>
</DataGrid>
Class BindingProxy:
namespace MyApp.Classes
{
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
}
UPDATE 2:
Dependency object class:
namespace My.WPF.App.Classes
{
public class BridgeDO: DependencyObject
{
public DataGridLength DataComandaColWidth
{
get { return (DataGridLength)GetValue(DataComandaColWidthProperty); }
set { SetValue(DataComandaColWidthProperty, value); }
}
public static readonly DependencyProperty EntryDateColWidthProperty =
DependencyProperty.Register("EntryDateColWidth",
typeof(DataGridLength),
typeof(BridgeDO),
new PropertyMetadata(new DataGridLength(1, DataGridLengthUnitType.Auto)));
}
}
Instance in the resource dictionary (DictionaryDO.xaml):
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:My.WPF.App.Classes">
<local:BridgeDO x:Key="DO"/>
</ResourceDictionary>
Merging it into resource dictionary (app.xaml) :
<Application x:Class="My.WPF.Apps.MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="http://schemas.microsoft.com/wpf/2008/toolkit"
xmlns:local="clr-namespace:My.WPF.Apps.MyApp"
StartupUri="Main.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionaries/DictionaryDO.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Styles -->
</ResourceDictionary>
</Application.Resources>
</Application>
Window :
<Window x:Name="MainWindow" x:Class="My.WPF.Apps.MyApp.wMain"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Window.Resources>
<!-- Resources -->
</Window.Resources>
<DataGrid Grid.Row="1"
AutoGenerateColumns="False"
ItemsSource="{Binding Path=MyListOfItems}"
VerticalAlignment="Stretch" IsReadOnly="True"
SelectionMode="Single" ColumnWidth="*">
<DataGrid.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
</DataGrid.Resources>
<DataGridTextColumn
CellStyle="{StaticResource MyDataGridCellStyle}"
Binding="{Binding Path=EntryDate, StringFormat=\{0:dd/MM/yyyy\}}"
Header="Entry Date"
Width="{Binding EntryDateColWidth, Source={StaticResource DO}}"
HeaderStyle="{DynamicResource CenterGridHeaderStyle}">
</DataGridTextColumn>
</DataGrid>
</Window>
View model:
public class myMainViewModel : ViewModelBase
{
private BridgeDO _do;
public myMainViewModel(IView view)
{
_view = view;
_do = Application.Current.Resources["DO"] as BridgeDO;
}
private void BackgroundWorker_DoWork()
{
// Do some stuff
SetColumnWidth();
}
private void SetColumnWidth()
{
_view.GetWindow().Dispatcher.Invoke(new Action(delegate
{
bool isEmpty = this.MyListOfItems.TrueForAll(e => !e.EntryDate.HasValue);
_do.SetValue(BridgeDO.EntryDateColWidthProperty, isEmpty ? new DataGridLength(22.0) : new DataGridLength(1, DataGridLengthUnitType.Auto));
}), DispatcherPriority.Render);
}
}
But column width is not being updated...

OK, this demonstrates the principle of what I'm describing and it's a bit quick n dirty.
Define a dependency object as a new class.
using System.Windows;
using System.Windows.Controls;
namespace wpf_12
{
public class BridgeDO : DependencyObject
{
public DataGridLength ColWidth
{
get { return (DataGridLength)GetValue(ColWidthProperty); }
set { SetValue(ColWidthProperty, value); }
}
public static readonly DependencyProperty ColWidthProperty =
DependencyProperty.Register("ColWidth", typeof(DataGridLength), typeof(BridgeDO), new PropertyMetadata(new DataGridLength(20.0)));
}
}
Create an instance in a resource dictionary.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:wpf_12">
<local:BridgeDO x:Key="DO"/>
</ResourceDictionary>
Merge that resource dictionary in app.xaml:
<Application x:Class="wpf_12.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:wpf_12"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Quick and dirty viewmodel, this will change the column width to auto 6 seconds after it's instantiated.
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace wpf_12
{
public class MainWIndowViewModel
{
public ObservableCollection<object> Items { get; set; } = new ObservableCollection<object>
{ new { Name="Billy Bob", ID=1},
new { Name="Peter Parker", ID=2},
new { Name="Sherlock Holmes", ID=2}
};
public MainWIndowViewModel()
{
ChangeWidth();
}
private async void ChangeWidth()
{
await Task.Delay(6000);
var _do = Application.Current.Resources["DO"] as BridgeDO;
_do.SetCurrentValue(BridgeDO.ColWidthProperty, new DataGridLength(1, DataGridLengthUnitType.Auto));
}
}
}
Use that in my window:
Name="Window"
>
<Window.DataContext>
<local:MainWIndowViewModel/>
</Window.DataContext>
<Window.Resources>
</Window.Resources>
<Grid>
<DataGrid ItemsSource="{Binding Items}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}" Width="{Binding ColWidth, Source={StaticResource DO}}"/>
<DataGridTextColumn Binding="{Binding ID}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
When I run this I start off with a narrow-ish column. Sit there watching it for a while and it changes to auto width and widens.

I thought that using a converter it will be a good idea but converter is never called. Why?
Because the DataGridTextColumn inherits no DataContext and can't bind to the EntryDate property like that.
My final goal is to set column width to 20 when all its cells are empty, otherwise set its width to 50.
Then you could iterate through all items in the DataGrid's ItemsSource and check the value of their EntryDate property, e.g.:
dgg.Loaded += (s, e) =>
{
bool isEmpty = true;
foreach(var item in dgg.Items.OfType<Item>())
{
if (!string.IsNullOrEmpty(item.EntryDate))
{
isEmpty = false;
break;
}
}
//set the Width of the column (at index 0 in this sample)
dgg.Columns[0].Width = isEmpty? 20 : 500;
};
Note: In this particular example I have assumed that EntryDate is indeed a string. If it's a DateTime or a Nullable<DateTime> you could check whether it equals default(DateTime) or default(DateTime?) respectively.

Related

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.

Populate ComboBox of a DataGrid with values from another column

I am an experienced user of Winforms and I am now playing with WPF, but I am struggling a bit.
I have a datagrid with 2 columns.
The first column is a DataGridTextColumn which is bound to the "Name" property of my object.
The second column is bound to the "Power" property of my object.
When the user edits the Power column, I would like to display a combo box that lists all the Name of the first column in addition of "None" as first item.
How can I do that?
In addition, if the user updates any Name of the first column, I would like the change to be reflected in the Power column. Is it possible?
In code behind:
public partial class MainWindow : Window
{
ObservableCollection<MyObject> objects = new ObservableCollection<MyObject>();
public MainWindow()
{
InitializeComponent();
dgObjects.ItemsSource = objects;
}
}
public class MyObject
{
public String Name { get; set; }
public String Power { get; set; }
}
In Xaml:
<DataGrid Name="dgObjects" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridComboBoxColumn Header="Power" Binding="????"/>
</DataGrid.Columns>
</DataGrid>
Thanks
You can do this via binding on the DataContext. Though, first you need to set up the Window's DataContext properly. ItemsSource should not be done via code-behind (this is WPF, use binding!).
Set up your class structure like:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class ViewModel
{
public ObservableCollection<MyObject> Objects { get; } = new ObservableCollection<MyObject>();
public ViewModel()
{
Objects.Add(new MyObject
{
Name = "Name"
});
Objects.Add(new MyObject
{
Name = "Name2"
});
Objects.Add(new MyObject
{
Name = "Name3"
});
}
}
public class MyObject
{
public String Name { get; set; }
public String Power { get; set; }
}
Now, update your xaml. You will need to use a CellTemplate and a standard Combobox. The ComboBox will then bind to the Objects collection, display the Name, but set the value in Power.
<Window x:Class="WpfApplication8.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"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
x:Name="MyWindow">
<Grid>
<DataGrid Name="dgObjects" AutoGenerateColumns="False"
ItemsSource="{Binding Objects}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding ElementName=MyWindow, Path=DataContext.Objects}"
DisplayMemberPath="Name"
SelectedItem="{Binding Power}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>

Binding visibility to bool value in WPF dataGrid

I am aware that as DataGrid columns aren't part of the Visual Tree, you can't bind the visibility property of the columns directly to a boolean property in your VM. You have to do it another way. Below is the way I have done it:
public class LocalVm
{
public static ObservableCollection<Item> Items
{
get
{
return new ObservableCollection<Item>
{
new Item{Name="Test1", ShortDescription = "Short1"}
};
}
}
public static bool IsDetailsModeEnabled
{
get { return true; }
}
}
public class Item : INotifyPropertyChanged
{
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged();
}
}
private string _shortDescription;
public string ShortDescription
{
get
{
return _shortDescription;
}
set
{
_shortDescription = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<Window x:Class="Wpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Wpf"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignInstance Type=local:LocalVm}"
Title="MainWindow" Height="350" Width="525"
Name="MyWindow">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVis" />
<DataGridTextColumn x:Key="ThatPeskyColumn"
Binding="{Binding Size}"
Visibility="{Binding DataContext.IsDetailsModeEnabled,
Source={x:Reference MyWindow},
Converter={StaticResource BoolToVis}}"/>
</Window.Resources>
<Grid>
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Items}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}" />
<StaticResource ResourceKey="ThatPeskyColumn"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
However, in my xaml window, there is an error on the "visibility" property: "Object reference not set to an instance of an object". If I remove the source and converter part, the error goes but it doesn't bind properly. What I am doing wrong?
thanks in advance
As I can see from the code you've provided, IsDetailsModeEnabled property is static. To bind to static property in that case, you may just create your VM as a static resource, bind to its static property and set it to Window DataContext later:
<Window.Resources>
<local:LocalVm x:Key="vm" />
<BooleanToVisibilityConverter x:Key="BoolToVis" />
<DataGridTextColumn x:Key="ThatPeskyColumn"
Binding="{Binding ShortDescription}"
Visibility="{Binding Path = IsDetailsModeEnabled,
Source={StaticResource vm},
Converter={StaticResource BoolToVis}}"/>
</Window.Resources>
<Window.DataContext>
<StaticResource ResourceKey="vm"/>
</Window.DataContext>
From the other hand, more classical approach in that case would be with a proxy Freezable object, described here:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
XAML
<Window.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
<BooleanToVisibilityConverter x:Key="BoolToVis" />
<DataGridTextColumn x:Key="ThatPeskyColumn"
Binding="{Binding ShortDescription}"
Visibility="{Binding Path=Data.IsDetailsModeEnabled,
Source={StaticResource proxy},
Converter={StaticResource BoolToVis}}"/>
</Window.Resources>

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.

Binding a property to the change the listbox items foreground

I have the following ViewModel and I'd like to bind the HotkeysForeground to change the color in the ListBox.
namespace Monkey.Core.ViewModel
{
using System;
using System.Collections.ObjectModel;
using System.Windows.Media;
using Monkey.Core.SystemMonitor.Accessibility;
using Monkey.Core.SystemMonitor.Input;
public class MainWindowViewModel : WorkspaceViewModel
{
private readonly FocusManager _focusManager;
private readonly HotkeyManager _hotkeyManager;
private readonly ObservableCollection<string> _hotkeys;
private Brush _foregroundColor;
private string _title;
public MainWindowViewModel()
{
_hotkeys = new ObservableCollection<string>();
_hotkeyManager = new HotkeyManager();
_hotkeyManager.NewHotkey += HotkeyManager_NewHotkey;
_focusManager = new FocusManager();
_focusManager.Focus += FocusManager_Focus;
}
public Brush HotkeysForeground
{
get
{
return _foregroundColor;
}
set
{
_foregroundColor = value;
OnPropertyChanged(() => Title);
}
}
public ReadOnlyObservableCollection<string> Hotkeys
{
get
{
return new ReadOnlyObservableCollection<string>(_hotkeys);
}
}
public string Title
{
get
{
return _title;
}
set
{
_title = value;
OnPropertyChanged(() => Title);
}
}
protected override void OnDispose()
{
base.OnDispose();
_hotkeyManager.Dispose();
_focusManager.Dispose();
}
private void FocusManager_Focus(object sender, FocusManagerEventArgs e)
{
Title = e.Title;
}
private void HotkeyManager_NewHotkey(object sender, EventArgs eventArgs)
{
HotkeysForeground = Brushes.Blue;
_hotkeys.Clear();
foreach (var hotkey in _hotkeyManager.GetHotkeys())
{
_hotkeys.Add(hotkey);
}
}
}
}
I want to change the foreground color of the items in the ListBox every time the "HotkeyManager_NewHotkey" is fired, to some reason I can't seems to bind it to the view, I tried multiple things to make it work to no avail.
Here is the View I have.
<Window x:Class="Monkey.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="{Binding Mode=OneWay, Path=Title, UpdateSourceTrigger=PropertyChanged}" Height="200" Width="200" ShowInTaskbar="False" WindowStyle="ToolWindow" Topmost="True" ResizeMode="CanResizeWithGrip" AllowsTransparency="False">
<ListBox
Canvas.Left="110"
Canvas.Top="74"
Name="HotkeyList"
Height="Auto" Width="Auto" HorizontalContentAlignment="Left"
BorderThickness="0"
ScrollViewer.CanContentScroll="False"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
ItemsSource="{Binding Path=Hotkeys}" VerticalAlignment="Stretch" VerticalContentAlignment="Center" FontSize="20">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsEnabled" Value="False" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Window>
I'm fairly new to WPF and haven't really explored bindings into depth so any help is appreciated.
For starters, you're notifying about Title property change in HotkeysForeground. However, this might not be the case.
If fixing that doesn't help, this rather lenghty way should work for you:
change HotkeysForeground property type to string (just store color name)
create static resource brush in XAML, bind it to color name
override listbox item template to something fairly simple (eg. Label) and bind its foreground to previously mentioned bursh
So, applying those changes:
public string HotkeysForeground
{
get { return _foregroundColor; }
set
{
_foregroundColor = value;
// I assume this is some smart workaround to INPC...
OnPropertyChanged(() => HotkeysForeground);
}
}
Now, in XAML you'll have to do this:
<!-- need to import System namespace -->
<Window x:Class="Monkey.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Window.Resources>
<SolidColorBrush Color="{Binding HotkeysForeground}" x:Key="HotkeysBrush"/>
</Window.Resources>
<ListBox ItemsSource="{Binding Path=Hotkeys}">
<ListBox.ItemTemplate>
<!
<DataTemplate DataType="{x:Type sys:String}">
<Label Content="{Binding}"
Foreground="{StaticResource HotkeysBrush}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Window>

Resources