Style all DataGridTextColumns via AttachedProperty - c#

What I tried to do is create a Style to apply a WordWrap on all DataGridTextColumns in a Datagrid without explicitly setting it like this.
<DataGrid ItemsSource="{Binding Lines}">
<DataGrid.Columns>
<DataGridTextColumn Header="Column1" Binding="{Binding Path=Result1}">
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
Unfortunately it is not possible to use some Style as below directly, because DataGridTextColumn isn't a FrameworkElement.
<Style TargetType="{x:Type TextBlock}" x:Key="WrapText">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
I found this workaround https://stackoverflow.com/a/2640862/5381620 by RayBurns and was trying to figure out how it's working. However, I'm new to attached properties and therefore don't understand why it is not working.
The c# code seems to be ok.
public class MyDataGridHelper : DependencyObject
{
private static readonly DependencyProperty TextColumnStyleProperty = DependencyProperty.RegisterAttached("TextColumnStyle", typeof(Style), typeof(MyDataGridHelper), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
var grid = (DataGrid)obj;
if (e.OldValue == null && e.NewValue != null)
grid.Columns.CollectionChanged += (obj2, e2) =>
{
UpdateColumnStyles(grid);
};
}
});
public static void SetTextColumnStyle(DependencyObject element, Style value)
{
element.SetValue(TextColumnStyleProperty, value);
}
public static Style GetTextColumnStyle(DependencyObject element)
{
return (Style)element.GetValue(TextColumnStyleProperty);
}
private static void UpdateColumnStyles(DataGrid grid)
{
var style = GetTextColumnStyle(grid);
foreach (var column in grid.Columns.OfType<DataGridTextColumn>())
foreach (var setter in style.Setters.OfType<Setter>())
if (setter.Value is BindingBase)
BindingOperations.SetBinding(column, setter.Property, (BindingBase)setter.Value);
else
column.SetValue(setter.Property, setter.Value);
}
}
I got totally confused is when we get towards figuring out the style setter.
Currently I'm trying it this way, which is obviously not working, but actually I don't have a clue what this targettype should really look like.
<local:MyDataGridHelper.TextColumnStyle>
<Style TargetType="FrameworkElement">
<Setter Property="TextBlock.TextWrapping" Value="Wrap"/>
</Style>
</local:MyDataGridHelper.TextColumnStyle>

You should set the ElementStyle of the columns to the value of the attached property:
public class MyDataGridHelper : DependencyObject
{
private static readonly DependencyProperty TextColumnStyleProperty =
DependencyProperty.RegisterAttached("TextColumnStyle", typeof(Style), typeof(MyDataGridHelper), new PropertyMetadata
{
PropertyChangedCallback = (obj, e) =>
{
var grid = (DataGrid)obj;
if (e.OldValue == null && e.NewValue != null)
grid.Columns.CollectionChanged += (obj2, e2) =>
{
UpdateColumnStyles(grid);
};
}
});
public static void SetTextColumnStyle(DependencyObject element, Style value)
{
element.SetValue(TextColumnStyleProperty, value);
}
public static Style GetTextColumnStyle(DependencyObject element)
{
return (Style)element.GetValue(TextColumnStyleProperty);
}
private static void UpdateColumnStyles(DataGrid grid)
{
var style = GetTextColumnStyle(grid);
foreach (var column in grid.Columns.OfType<DataGridTextColumn>())
column.ElementStyle = style;
}
}
Usage:
<DataGrid>
<local:MyDataGridHelper.TextColumnStyle>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
</local:MyDataGridHelper.TextColumnStyle>
...
</DataGrid>

Related

WPF DataGrid - How to setup correct DataTrigger binding to cell's data source (and not row's source)

Trying to setup the background of a cell dependend on a cell-object property in a WPF DataGrid I get an error, that the property is not found (but on the row-object):
System.Windows.Data Error: 40 : BindingExpression path error: 'IsOn' property not found on 'object' ''MyRow' (HashCode=48826322)'. BindingExpression:Path=IsOn; DataItem='MyRow' (HashCode=48826322); target element is 'DataGridCell' (Name=''); target property is 'NoTarget' (type 'Object')
I wonder, why the DataTrigger Binding is addressing the row object "MyRow", since the DataTrigger is defined for/inside a CellStyle.
XAML:
<DataGrid Name="tblTest" Grid.Column="2" IsReadOnly="True" AutoGenerateColumns="True">
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background" Value="PaleGreen" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsOn}" Value="True">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
</DataGrid>
C#
class MyCell
{
public MyCell( string v)
{
Value = v;
}
public string Value { get; set; }
public bool IsOn { get => Value == "one"; }
public override string ToString()
{
return Value;
}
}
class MyRow
{
public MyCell One { get; set; }
public MyCell Two { get; set; }
}
void SetupTestTable()
{
List<MyRow> data = new();
data.Add(new MyRow
{
One = new MyCell("one"),
Two = new MyCell("two")
});
tblTest.ItemsSource = data;
}
So how to bind against the cell object "MyCell" correctly?
DataGridCells have the same DataContext as DataGridRow - there are many obstacles to do differently in general-purpose manner. So single DataGrid.CellStyle won't work
I will use AutoGeneratingColumn to create cell styles for each column. However they will be based on existing style which is stored in DataGrid.Resources.
<DataGrid Name="tblTest" Grid.Column="2" IsReadOnly="True"
AutoGenerateColumns="True"
AutoGeneratingColumn="tblTest_AutoGeneratingColumn">
<DataGrid.Resources>
<Style TargetType="{x:Type DataGridCell}" x:Key="ColoredCellStyle">
<Setter Property="Background" Value="Cyan" />
<Style.Triggers>
<DataTrigger Binding="{Binding Tag.IsOn, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
</DataGrid>
I'm using binding to Tag instead of DataContext, because DataContext is MyRow object. In Tag there will be MyCell objects. It is achieved in event handler:
private void tblTest_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column is DataGridTextColumn tc && tc.Binding is Binding binding)
{
// unique value for each column
var property = binding.Path.Path;
// DataGrid reference to get Resources
var dg = (DataGrid)sender;
// new cell style which inherits trigger from ColoredCellStyle and binds Tag to MyCell property
var cellStyle = new Style
{
TargetType = typeof(DataGridCell),
BasedOn = (Style)dg.Resources["ColoredCellStyle"],
Setters =
{
new Setter
{
Property = DataGridCell.TagProperty,
Value = new Binding(property)
}
}
};
tc.CellStyle = cellStyle;
};
}

Trigger/Setter on dependency property reached by Binding

I want to set a dependency property that I declared in my Class1 that inherits from DependencyObject:
public static readonly DependencyProperty MyMouseOverProperty = DependencyProperty.Register("MyMouseOver", typeof(bool), typeof(Class1),
new PropertyMetadata(false,new PropertyChangedCallback(On_MyMouseOver)));
private static void On_MyMouseOver(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// some code here
}
public bool MyMouseOver
{
get { return (bool)GetValue(MyMouseOverProperty); }
set { SetValue(MyMouseOverProperty, value); }
}
I'll use "MyMouseOver" in XAML in order to use its state in "On_MyMouseOver" to affect another Object.
<DataTemplate DataType="{x:Type local:Class1}">
<Canvas x:Name="Canvas_Classe1"
Background="Transparent">
<Canvas.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="{Binding Path=MyMouseOver}" Value="True"/>
</Trigger>
</Canvas.Triggers>
</Canvas>
but ... this is not working.
How to use Setter to access to a DependencyProperty declared not within a control but a class ?
You can accomplish this with attached property
public class Class1
{
public static readonly DependencyProperty MyMouseOverProperty = DependencyProperty.RegisterAttached(
"MyMouseOver", typeof(bool), typeof(Class1), new FrameworkPropertyMetadata(false, PropertyChangedCallback)
);
public static void SetMyMouseOver(UIElement element, Boolean value)
{
element.SetValue(MyMouseOverProperty, value);
}
public static bool GetMyMouseOver(UIElement element)
{
return (bool)element.GetValue(MyMouseOverProperty);
}
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// some code here
}
}
Set attached property on DataTemplate IsMouseOver
<DataTemplate DataType="{x:Type local:Class1}">
<Canvas x:Name="Canvas_Classe1"
Background="Black">
</Canvas>
<DataTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="local:Class1.MyMouseOver" Value="True"/>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>

Mouse Double Click DataGrid row

I have a DataGrid style template that I wish to add double click behaviour to. The binding should be correct but I cannot seem to get the xaml compiling / working.
All objects added to an IDictionary must have a Key attribute
or some other type of key associated with them.
What is wrong with the code below?
<Style TargetType="{x:Type DataGridRow}">
<EventSetter Event="MouseDoubleClick" Handler="{Binding Connect}"/>
Update per Viktor's comment (gives exact same error):
<Style x:Key="dataGridRowStyle" TargetType="{x:Type DataGridRow}">
<EventSetter Event="PreviewMouseDoubleClick" Handler="{Binding Connect}"/>
One can use DataGrid InputBindings to achieve goal:
<DataGrid.InputBindings>
<MouseBinding MouseAction="LeftDoubleClick" Command="{Binding SomeCommand}" />
</DataGrid.InputBindings>
You can apply the following behavior on data grid row and follow the usage for implementation.
Double Click Behavior
public class DoubleClickBehavior
{
#region DoubleClick
public static DependencyProperty OnDoubleClickProperty = DependencyProperty.RegisterAttached(
"OnDoubleClick",
typeof(ICommand),
typeof(DoubleClickBehavior),
new UIPropertyMetadata(DoubleClickBehavior.OnDoubleClick));
public static void SetOnDoubleClick(DependencyObject target, ICommand value)
{
target.SetValue(OnDoubleClickProperty, value);
}
private static void OnDoubleClick(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
var element = target as Control;
if (element == null)
{
throw new InvalidOperationException("This behavior can be attached to a Control item only.");
}
if ((e.NewValue != null) && (e.OldValue == null))
{
element.MouseDoubleClick += MouseDoubleClick;
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
element.MouseDoubleClick -= MouseDoubleClick;
}
}
private static void MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
UIElement element = (UIElement)sender;
ICommand command = (ICommand)element.GetValue(OnDoubleClickProperty);
command.Execute(null);
}
#endregion DoubleClick
}
Usage
<Style BasedOn="{StaticResource {x:Type DataGridRow}}"
TargetType="{x:Type DataGridRow}">
<Setter Property="Helpers:DoubleClickBehavior.OnDoubleClick" Value="{Binding Path=DataContext.MyCommandInVM, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ViewLayer:MyUserControl}}}" />
</Style>
Not sure if you're going the MVVM route, but I've achieved this functionality using an Attached Command Behavior to wire up the double click event to a command in my viewmodel (where "command" is a reference to my attachedCommandBehavior assembly/class):
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="command:CommandBehavior.Event" Value="MouseDoubleClick"/>
<Setter Property="command:CommandBehavior.Command" Value="{Binding SelectItemCmd}"/>
<Setter Property="command:CommandBehavior.CommandParameter" Value="{Binding }"/>
</Style>
</DataGrid.RowStyle>

WPF UserControls; triggers and changing other controls

I've created a WPF UserControl which contains a Button and a ComboBox. I'd like to change the style of both, depending on the position of the mouse, so the UIElement with the mouse over is coloured Black and the other is coloured Red. If neither are styled then the default styling will apply.
Don't worry, this nightmarish colour scheme is just to illustrate the concept!
Thanks in advance for your help.
XAML
<UserControl x:Class="WpfUserControlSample.ToolbarButtonCombo"
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:WpfUserControlSample"
x:Name="Control"
mc:Ignorable="d"
d:DesignHeight="30">
<UserControl.Resources>
<Style TargetType="{x:Type local:ToolbarButtonCombo}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsButtonMouseOver}" Value="True">
<Setter Property="ButtonStyle" Value="Black"/>
<Setter Property="ComboStyle" Value="Red"/>
</DataTrigger>
<!--
<DataTrigger Binding="{Binding IsComboMouseOver}" Value="True">
<Setter Property="ButtonStyle" Value="Red"/>
<Setter Property="ComboStyle" Value="Black"/>
</DataTrigger>
-->
</Style.Triggers>
</Style>
</UserControl.Resources>
<StackPanel Orientation="Horizontal" Height="30">
<Button Name="btn" Background="{Binding ButtonStyle,ElementName=Control,Mode=OneWay}">
Test
</Button>
<ComboBox Name="cmb" Background="{Binding ComboStyle,ElementName=Control,Mode=OneWay}"></ComboBox>
</StackPanel>
</UserControl>
Codebehind:
namespace WpfUserControlSample
{
public partial class ToolbarButtonCombo : UserControl, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public ToolbarButtonCombo()
{
InitializeComponent();
btn.MouseEnter += new MouseEventHandler(btn_MouseChanged);
btn.MouseLeave += new MouseEventHandler(btn_MouseChanged);
}
void btn_MouseChanged(object sender, MouseEventArgs e)
{
OnPropertyChanged("IsButtonMouseOver");
}
public bool IsButtonMouseOver
{
get { return btn.IsMouseOver; }
}
public static readonly DependencyProperty IsButtonMouseOverProperty =
DependencyProperty.Register("IsButtonMouseOver", typeof(string), typeof(ToolbarButtonCombo), new PropertyMetadata("false"));
public string ButtonStyle { get; set; }
public static readonly DependencyProperty ButtonStyleProperty =
DependencyProperty.Register("ButtonStyle", typeof(string), typeof(ToolbarButtonCombo));
public string ComboStyle { get; set; }
public static readonly DependencyProperty ComboStyleProperty =
DependencyProperty.Register("ComboStyle", typeof(string), typeof(ToolbarButtonCombo));
}
}
There are a two problems.
First your DataTrigger bindings do not look correct. They are looking for the IsButtonMouseOver on the DataContext, not the associated control. You'd need to use:
<DataTrigger Binding="{Binding IsButtonMouseOver, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="ButtonStyle" Value="Black"/>
<Setter Property="ComboStyle" Value="Red"/>
</DataTrigger>
Or:
<Trigger Property="IsButtonMouseOver" Value="True">
<Setter Property="ButtonStyle" Value="Black"/>
<Setter Property="ComboStyle" Value="Red"/>
</Trigger>
The other is your IsButtonMouseOver is not implemented correctly. You should do something like:
public static readonly DependencyProperty IsButtonMouseOverProperty = DependencyProperty.Register("IsButtonMouseOver",
typeof(bool), typeof(ToolbarButtonCombo), new PropertyMetadata(false));
public bool IsButtonMouseOver
{
get { return (bool)this.GetValue(IsButtonMouseOverProperty); }
set { this.SetValue(IsButtonMouseOverProperty, value); }
}
void btn_MouseChanged(object sender, MouseEventArgs e)
{
this.IsButtonMouseOver = this.btn.IsMouseOver;
}
Or even more correctly, make the IsButtonMouseOver a read-only dependency property like so:
private static readonly DependencyPropertyKey IsButtonMouseOverPropertyKey = DependencyProperty.RegisterReadOnly("IsButtonMouseOver",
typeof(bool), typeof(ToolbarButtonCombo), new FrameworkPropertyMetadata(false));
public static readonly DependencyProperty IsButtonMouseOverProperty = ToolbarButtonCombo.IsButtonMouseOverPropertyKey.DependencyProperty;
public bool IsButtonMouseOver {
get { return (bool)this.GetValue(IsButtonMouseOverProperty); }
private set { this.SetValue(IsButtonMouseOverPropertyKey, value); }
}
Your other properties (ButtonStyle and ComboStyle) would need to be properly implemented also, and their get/set methods are not backed by the dependency property.

Apply a default resource style to all DataGridTextColumns

I have several columns that are numeric and want them to be right justified. Here is a somewhat contrived example demonstrating my issue:
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Controls="clr-namespace:System.Windows.Controls;assembly=PresentationFramework" xmlns:Windows="clr-namespace:System.Windows;assembly=PresentationFramework" xmlns:Data="clr-namespace:System.Windows.Data;assembly=PresentationFramework" Title="MyApp"
Width="Auto" Height="Auto" SizeToContent="WidthAndHeight" Loaded="Window_Loaded">
<Controls:DataGrid Name="MyDataGrid" IsReadOnly="True"
xmlns="http://schemas.microsoft.com/wpf/2008/toolkit" ItemsSource="{Data:Binding}" AutoGenerateColumns="True">
<Controls:DataGrid.Columns>
<!-- This is a product name and is left justified by default -->
<Controls:DataGridTextColumn Header="ProductName" Binding="{Data:Binding Path=ProductName}" />
<!-- The rest of the columns are numeric and I would like for them to be right justified -->
<Controls:DataGridTextColumn Header="ProductId1" Binding="{Data:Binding Path=ProductId1}" >
<Controls:DataGridTextColumn.ElementStyle>
<Windows:Style TargetType="Controls:TextBlock">
<Windows:Setter Property="HorizontalAlignment" Value="Right"/>
</Windows:Style>
</Controls:DataGridTextColumn.ElementStyle>
</Controls:DataGridTextColumn>
<Controls:DataGridTextColumn Header="ProductId2" Binding="{Data:Binding Path=ProductId2}" >
<Controls:DataGridTextColumn.ElementStyle>
<Windows:Style TargetType="Controls:TextBlock">
<Windows:Setter Property="HorizontalAlignment" Value="Right"/>
</Windows:Style>
</Controls:DataGridTextColumn.ElementStyle>
</Controls:DataGridTextColumn>
<Controls:DataGridTextColumn Header="ProductId3" Binding="{Data:Binding Path=ProductId3}" >
<Controls:DataGridTextColumn.ElementStyle>
<Windows:Style TargetType="Controls:TextBlock">
<Windows:Setter Property="HorizontalAlignment" Value="Right"/>
</Windows:Style>
</Controls:DataGridTextColumn.ElementStyle>
</Controls:DataGridTextColumn>
<!-- More numeric columns follow... -->
</Controls:DataGrid.Columns>
</Controls:DataGrid>
</Window>
The right justification styles are repeated for all but the first column and seem redundant. If only I could set the DataGridTextColumns in this grid to be right justified, then I would only have to explicitly left justify the first column. How can I do this, perhaps using a style as a resource? Is there a better way?
DataGridTextColumn doesn't derive from FrameworkElement so you can't create a Style for it out-of-the-box.
The easiest way to do what you're after is to create a Style for DataGridCell and right align the TextBlock from there. Then set CellStyle={x:Null} (or any other style you might want) for the Columns that shouldn't have this
<Controls:DataGrid ...>
<Controls:DataGrid.Resources>
<Windows.Style TargetType="Controls:DataGridCell">
<Windows.Setter Property="TextBlock.HorizontalAlignment" Value="Right"/>
</Windows.Style>
</Controls:DataGrid.Resources>
<Controls:DataGrid.Columns>
<!-- This is a product name and is left justified by default -->
<Controls:DataGridTextColumn Header="ProductName"
Binding="{Binding Path=ProductName}"
CellStyle="{x:Null}"/>
Update
But if you do want to apply a Style to a DataGridTextColumn something like this is required.
First we need a helper class that can "hold the Style". In it, we add all the properties we would like to be able to Style (that isn't in FrameworkElement). In this case ElementStyle.
public class DataGridTextColumnStyleHelper : FrameworkElement
{
public DataGridTextColumnStyleHelper(){}
public static readonly DependencyProperty ElementStyleProperty =
DependencyProperty.Register(
"ElementStyle",
typeof(Style),
typeof(DataGridTextColumnStyleHelper));
public Style ElementStyle
{
get { return (Style)GetValue(ElementStyleProperty); }
set { SetValue(ElementStyleProperty, value); }
}
}
Then we add the Style in xaml
<Style x:Key="DataGridTextColumnStyle"
TargetType="local:DataGridTextColumnStyleHelper">
<Setter Property="ElementStyle">
<Setter.Value>
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
</Setter.Value>
</Setter>
</Style>
And to be able to apply this Style on a DataGridTextColumn we need another Helper class with a TextColumnStyle property. In it, we apply the style using reflection and SetValue.
public class MyDataGridHelper : DependencyObject
{
private static readonly DependencyProperty TextColumnStyleProperty = DependencyProperty.RegisterAttached(
"TextColumnStyle",
typeof(Style),
typeof(MyDataGridHelper),
new PropertyMetadata(MyPropertyChangedCallback));
public static void SetTextColumnStyle(DependencyObject element, string value)
{
element.SetValue(TextColumnStyleProperty, value);
}
public static Style GetTextColumnStyle(DependencyObject element)
{
return (Style)element.GetValue(TextColumnStyleProperty);
}
private static void MyPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(d) == true)
{
return;
}
DataGridTextColumn textColumn = (DataGridTextColumn)d;
Style textColumnStyle = e.NewValue as Style;
foreach (SetterBase setterBase in textColumnStyle.Setters)
{
if (setterBase is Setter)
{
Setter setter = setterBase as Setter;
if (setter.Value is BindingBase)
{
//Not done yet..
}
else
{
Type type = textColumn.GetType();
PropertyInfo propertyInfo = type.GetProperty(setter.Property.Name);
propertyInfo.SetValue(textColumn, setter.Value, null);
}
}
}
}
}
Finally, we can use the Style on a DataGridTextColumn like this
<DataGridTextColumn Header="ProductId1" Binding="{Binding Path=ProductId1}"
local:MyDataGridHelper.TextColumnStyle="{StaticResource DataGridTextColumnStyle}">

Categories

Resources