I am trying to resize the column width of a datagrid with a converter. For testing, I have a tets application that has a checkbox to tell if use the value of the converter or not. And a textbox to tell which is the width to apply.
When I modify the checkbox or the textbox, the properties of the view model are notified, but the converter is not fired.
My code is this:
Main windows:
<Window x:Class="ModificarColumnaDataGrid.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:ModificarColumnaDataGrid"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<local:ucControlUsuario HorizontalAlignment="Stretch" Margin="0,0,0,0" VerticalAlignment="Stretch"/>
</Grid>
</Window>
User control:
<UserControl x:Class="ModificarColumnaDataGrid.ucControlUsuario"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ModificarColumnaDataGrid"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<!--Este diccionario, el GUI, tendrá la configuración general para los diferentes elementos que se tengan en la GUI.-->
<ResourceDictionary.MergedDictionaries>
<!--Ruta relativa, porque esta vista está en subdirectorio, al mismo nivel que recursos.-->
<ResourceDictionary Source="DiccionarioRecursos.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<CheckBox Content="Use Converter" HorizontalAlignment="Left" Margin="0,10,0,0" VerticalAlignment="Top"
IsChecked="{Binding UseConverterIsChecked}"/>
<TextBox Text="{Binding WidthColumn2, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="106,48,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<!--Necesario para poder acceder a las propiedades del datacontext, necesario para los datatriggers.-->
<FrameworkElement x:Name="ProxyElement" Visibility="Collapsed"/>
<DataGrid Margin="0,154,0,0" AutoGenerateColumns="false">
<DataGrid.Columns>
<DataGridTextColumn Header="Columna1" Width="200"/>
<DataGridTextColumn Header="Columna2" Width="200">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<!--<Setter Property="Width" Value="20"/>-->
<Setter Property="Width">
<Setter.Value>
<MultiBinding Converter="{StaticResource MiMultiValueConverter}" >
<Binding Source="{x:Reference ProxyElement}" Path="DataContext.UseConverterIsChecked" />
<Binding Source="{x:Reference ProxyElement}" Path="DataContext.WidthColumn2" />
</MultiBinding>
</Setter.Value>
</Setter>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
<DataGridTextColumn Header="Columna3" Width="200"/>
</DataGrid.Columns>
</DataGrid>
<Label Content="Pixels column 2" HorizontalAlignment="Left" Margin="0,44,0,0" VerticalAlignment="Top"/>
</Grid>
</UserControl>
Code behind of user control:
namespace ModificarColumnaDataGrid
{
/// <summary>
/// Lógica de interacción para ucControlUsuario.xaml
/// </summary>
public partial class ucControlUsuario : UserControl
{
public ucControlUsuario()
{
InitializeComponent();
DataContext = new ucControlUsuarioViewModel();
}
}
}
View model of user control:
namespace ModificarColumnaDataGrid
{
class ucControlUsuarioViewModel : BaseViewModel
{
private bool _useConverterIsChecked;
public bool UseConverterIsChecked
{
get { return _useConverterIsChecked; }
set
{
_useConverterIsChecked = value;
base.RaisePropertyChangedEvent(nameof(UseConverterIsChecked));
}
}
private double _widthColumn2;
public double WidthColumn2
{
get { return _widthColumn2; }
set
{
_widthColumn2 = value;
base.RaisePropertyChangedEvent(nameof(WidthColumn2));
}
}
}
}
View model base:
public abstract class BaseViewModel : INotifyPropertyChanging, INotifyPropertyChanged
{
#region INotifyPropertyChanging Members
public event PropertyChangingEventHandler PropertyChanging;
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Administrative Properties
/// <summary>
/// Whether the view model should ignore property-change events.
/// </summary>
public virtual bool IgnorePropertyChangeEvents { get; set; }
#endregion
#region Public Methods
//##ESTUDIAR: si esta implemantación es mejor.
//Este método tiene la ventaja de que en el set de la propiedad, con poner OnPropertyChanged() es suficiente, no hace falta
//pasar por parámetro ni el stirng con el nombre de la propiedad ni siquiera la propiedad, porque la coge por defecto.
//Pero se puede pasar la propiedad en caso de que quiera notificar fuera del set, pero no hace falta pasar el string, sino la
//propiedad, por lo que el mantenimiento es mucho mejor.
////////////public void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string paramStrNombrePropiedad = "")
////////////{
//////////// PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(paramStrNombrePropiedad));
////////////}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">The name of the changed property.</param>
public virtual void RaisePropertyChangedEvent(string propertyName)
{
// Exit if changes ignored
if (IgnorePropertyChangeEvents) return;
// Exit if no subscribers
if (PropertyChanged == null) return;
// Raise event
var e = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, e);
}
/// <summary>
/// Raises the PropertyChanging event.
/// </summary>
/// <param name="propertyName">The name of the changing property.</param>
public virtual void RaisePropertyChangingEvent(string propertyName)
{
// Exit if changes ignored
if (IgnorePropertyChangeEvents) return;
// Exit if no subscribers
if (PropertyChanging == null) return;
// Raise event
var e = new PropertyChangingEventArgs(propertyName);
PropertyChanging(this, e);
}
#endregion
}
Dicccionario de recursos:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:conv="clr-namespace:ModificarColumnaDataGrid"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<conv:MiMultiValueConverter x:Key="MiMultiValueConverter" />
</ResourceDictionary>
Multivalue converter:
namespace ModificarColumnaDataGrid
{
public class MiMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return 20;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
How could I make the converter be fired?
EDIT:
I have tried to set the width of the column istead of the textblock of the style:
<DataGridTextColumn.Width>
<MultiBinding Converter="{StaticResource MiMultiValueConverter}" >
<Binding Source="{StaticResource BindingProxy}" Path="Data.UseConverterIsChecked"/>
<Binding Source="{StaticResource BindingProxy}" Path="Data.WidthColumn2" />
</MultiBinding>
</DataGridTextColumn.Width>
But it doesn't work too.
However, if I use a converter directly in the Width property of the column it works:
<DataGridTextColumn Header="Columna2"
Width="{Binding Data.UseConverterIsChecked, Converter={StaticResource MiValueConverter}, Source={StaticResource BindingProxy}}">
The problem with thi solution is that I need a multivalue converter, and I don't know how to use it in the width property of the column.
Thanks.
The problem with thi solution is that I need a multivalue converter, and I don't know how to use it in the width property of the column.
Just set the Width property using object element syntax:
<DataGridTextColumn Header="Columna2">
<DataGridTextColumn.Width>
<MultiBinding Converter="{StaticResource MiValueConverter}">
<Binding Path="Data.UseConverterIsChecked" Source="{StaticResource BindingProxy}" />
<!-- add more bindings here -->
</MultiBinding>
</DataGridTextColumn.Width>
</DataGridTextColumn>
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 a UserControl and a ViewModel for it.
The UserControl is a simple Button Template, with an Ellipse in it. Basically a round button.
I use the ViewModel as a DataContext for the UserControl.
The ViewModel has a property called "State" whose value I use as a DataTrigger to change "Colors" of the Ellipse, these "Colors" are Dependency Properties in the UserControl.
Everything seems to work, but I cannot set the Default Colors for the Ellipse and because of that I don`t see anything in the Designer for the UserControl. See attached picture.
I do see the correct colors and the shapes when I place the UserControl on to the main window designer.
I definitely would like to see the shapes in the UserControl with some default values, so its easier to see what I`m working with.
This I believe is something to do with DataBinding "State" value from the ViewModel.
Here is the Code : UserControl xaml
<UserControl x:Name="thisBtn" x:Class="WpfAppDelMe.Views.CustomButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfAppDelMe.Views"
xmlns:viewmodel="clr-namespace:WpfAppDelMe.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.DataContext>
<viewmodel:CustButtonViewModel x:Name="xvm"/>
</UserControl.DataContext>
<Grid Background="#FF9EE3EA">
<Button Content="{Binding State}" Command="{Binding ClickCommand}" Foreground="#FFD13B3B" Margin="0" >
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Viewbox Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Grid.ColumnSpan="3">
<Ellipse Height="300" Width="300" Margin="5">
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}" >
<!-- Below Line does not work! -->
<Setter Property ="Fill" Value="Pink" />
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="{x:Null}">
<Setter Property="Fill" Value="OldLace"/>
</DataTrigger>
<DataTrigger Binding="{Binding State}" Value="0">
<Setter Property="Fill" Value="{Binding ElementName=thisBtn, Path=Color1}"/>
</DataTrigger>
<DataTrigger Binding="{Binding State}" Value="1">
<Setter Property="Fill" Value="{Binding ElementName=thisBtn, Path=Color2}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</Viewbox>
<!--The content presenter for the button string.
Placing this in a view box makes sure of the correct size.-->
<Viewbox Grid.Row="1" Grid.ColumnSpan="3" Margin="10">
<ContentPresenter/>
</Viewbox>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
UserControl .cs file
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfAppDelMe.Views
{
/// <summary>
/// Interaction logic for CustXam.xaml
/// </summary>
public partial class CustomButton : UserControl, INotifyPropertyChanged
{
/// <summary>
/// Interaction logic for the CustomButton UserControl
/// View and Model
/// ViewModel : CustomButtonViewModel
/// </summary>
public CustomButton()
{
InitializeComponent();
}
public static readonly DependencyProperty Color1Property =
DependencyProperty.Register("Color1", typeof(Brush), typeof(CustButton), new
PropertyMetadata(new SolidColorBrush(Colors.Black)));
public static readonly DependencyProperty Color2Property =
DependencyProperty.Register("Color2", typeof(Brush), typeof(CustButton), new
PropertyMetadata(new SolidColorBrush(Colors.White)));
public event PropertyChangedEventHandler PropertyChanged = delegate { };
//Null initialization is required because there are no listeners or
bindings
for this.
private int btnState=0;
//Button Color 1
public Brush Color1
{
get { return (Brush)GetValue(Color1Property); }
set { SetValue(Color1Property, value); }
}
public Brush Color2
{
get { return (Brush)GetValue(Color2Property); }
set { SetValue(Color2Property, value); }
}
public int BtnState
{
get
{ return btnState;}
set
{
btnState = value;
this.OnPropertyChanged("BtnState");
}
}
public void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
xvm.State = btnState;
}
}
}
}
[![enter image description here][1]][1]ViewModel for the UseControl
using System;
using System.ComponentModel;
using System.Windows.Input;
namespace WpfAppDelMe.ViewModels
{
/// <summary>
/// View Model class for the CustButton. All logic and calculations happen
here.
/// This is also the DataContext for the CustButton
///
/// View and Model : CustButton
/// ViewModel : CustButtonViewModel
/// </summary>
internal class CustButtonViewModel : INotifyPropertyChanged
{
public CustButtonViewModel()
{
}
private int state;
public event PropertyChangedEventHandler PropertyChanged;
private ICommand clickCommand;
public ICommand ClickCommand
{
get
{
if (clickCommand == null)
{
clickCommand = new RelayCommand(param => ChangeState(), param => CanChange());
}
return clickCommand;
}
}
/// <summary>
///
/// </summary>
private void ChangeState()
{
if (State == 0)
{
State = 1;
}
else
{
State = 0;
}
}
private bool CanChange()
{
return true;
}
//Button state
public int State
{
get{ return state; }
set
{
state = value;
OnPropertyChange("State");
}
}
/// <summary>
/// On Property Changed
/// </summary>
/// <param name="name"></param>
public void OnPropertyChange(string prop)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(prop));
}
}
}
}
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.
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 am having problem with the following code. I have a TreeView Control which is bound to a Collection. The TreeView does get populated with the desired results. HOwever the "IsSelected" property and ContextMenu's click Command is not firing. Following is the XAML code.
<UserControl x:Class="Plan.Views.PadView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:v="clr-namespace:Planner.Views"
xmlns:vm="clr-namespace:Planner.ViewModels"
<Grid>
<StackPanel Orientation="Vertical">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" OpacityMask="#FFECF5F5">
<TreeView ItemsSource="{Binding Pads}" Name="tree_View" Width="190">
<TreeView.ItemContainerStyle >
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding WellPadViewModel.IsSelected}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="Rename" Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type TreeView}}, Path=DataContext.RenameCommand}" />
</ContextMenu>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Members}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" >
<TextBlock.InputBindings>
<KeyBinding Key="F2" Command="{Binding RenameCommand}"/>
</TextBlock.InputBindings>
</TextBlock>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</UserControl>
And here is my ViewModel
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using System.ComponentModel;
using WPFApplication;
namespace FieldPlanner.ViewModels
{
public class PlanViewModel : BaseViewModel
{
Collection<Pads> pads = new Collection<Pads>();
public PlanViewModel()
{
IsSelected = true;
pads = new Collection<Pad>();
}
private ICommand _RenameCommand;
public ICommand RenameCommand
{
get
{
if (_RenameCommand == null)
{
_RenameCommand = new RelayCommand1((o) =>
{
// Your logic should go here
MessageBox.Show("Please rename me");
});
}
return _RenameCommand;
}
}
public ObservableCollection<PadInfo> Members { get; set; }
private static object _selectedItem = null;
// This is public get-only here but you could implement a public setter which also selects the item.
// Also this should be moved to an instance property on a VM for the whole tree, otherwise there will be conflicts for more than one tree.
public static object SelectedItem
{
get { return _selectedItem; }
private set
{
if (_selectedItem != value)
{
_selectedItem = value;
OnSelectedItemChanged();
}
}
}
public static void OnSelectedItemChanged()
{
// Raise event / do other things
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged("IsSelected");
if (_isSelected)
{
SelectedItem = this;
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
/// Class to hold the Pads info for a tree
/// </summary>
public class Pad
{
/// <summary>
/// Default Constructor
/// </summary>
public Pad()
{
this.Members = new ObservableCollection<PadInfo>();
}
/// <summary>
/// Name of the pad
/// </summary>
public string Name { get; set; }
/// <summary>
/// Members of the pad
/// </summary>
public ObservableCollection<PadInfo> Members { get; set; }
}
/// <summary>
/// Class to hold the well and slot IDs snapped to a pad
/// </summary>
public class PadInfo
{
/// <summary>
/// Slot ID
/// </summary>
public string SlotID { get; set; }
/// <summary>
/// Well ID
/// </summary>
public string WellID { get; set; }
}
public class RelayCommand1 : ICommand
{
#region Fields
readonly Action<object> _execute;
readonly Predicate<object> _canExecute;
#endregion // Fields
#region Constructors
public RelayCommand1(Action<object> execute)
: this(execute, null)
{
}
public RelayCommand1(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
throw new ArgumentNullException("execute");
_execute = execute;
_canExecute = canExecute;
}
#endregion // Constructors
#region ICommand Members
// [DebuggerStepThrough]
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion // ICommand Members
}
}
How can I identify the issue?
You have two problems:
IsSelected:
<Setter Property="IsSelected" Value="{Binding WellPadViewModel.IsSelected}" />
In TreeViewItem DataContext is set to instance of Pad and Pad doesn't have property IsSelected You have to do sth like this:
<Setter Property="IsSelected" Value="{Binding DataContext.IsSelected, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}" />
Problem with ContextMenu is much more sirious. ContextMenu isn't in VisualTree so you cannot bind to RelativeSource. Solution is here WPF Relative source- Cannot find source for binding with reference
Best regards
Please set the Tag property in your DataTemplate to TreeViewItem. I have sth like this:
<DataTemplate>
<Grid Width="270" Height="20" Tag="{Binding DataContext, RelativeSource = {RelativeSource AncestorType={x:Type UserControl}}}">
...
<Grid.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Edit">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<command:EventToCommand Command="{Binding Tag.YOURCOMMAND}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</MenuItem>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</DataTemplate>
It should work.