I have ContentPresenter with DataTemplateSelector:
...
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var model = item as ItemControlViewModel;
if (model.CurrentStatus == PrerequisitesStatus.Required)
{
return RequiredTemplate;
}
if (model.CurrentStatus == PrerequisitesStatus.Completed)
{
return FinishedTemplate;
}
...
return InProgressTemplate;
}
When CurrentStatus is changed, OnPropertyChanged is called.
I need somehow to trigger this DataTemplateSelector when the property is changed and change ContentPresenter DataTemplate. Any suggestions?
Threre are similar questions:
1
2, but I don't want to use any DataTriggers, because of too much states.
Tried to play with DataTriggers
<ContentPresenter
Grid.Column="1"
Height="16"
Width="16"
Margin="3">
<ContentPresenter.Triggers>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="0">
<Setter Property="ContentPresenter.ContentTemplate" Value="{StaticResource ResourceKey=_requiredStatusTemplate}" />
</DataTrigger>
</ContentPresenter.Triggers>
</ContentPresenter>
But got an error:
Triggers collection members must be of type EventTrigger :(
As you requested an example with datatriggers in the comments, here you are:
A FrameworkElement can only have EventTriggers, therefore you get the error Message Triggers collection members must be of type EventTrigger
And also don't use a ContentPresenter directly, it is meant to be used inside a ControlTemplate. Better use a ContentControl when you want to have dynamic content.
See What's the difference between ContentControl and ContentPresenter?
And finally here's a suggestion to your DataTrigger issue. I have put it inside a style for reusability ....
XAML :
<Window x:Class="WpfApplication88.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="requiredTemplate">
<TextBlock Text="requiredTemplate"></TextBlock>
<!--your stuff here-->
</DataTemplate>
<DataTemplate x:Key="completedTemplate">
<TextBlock Text="CompletedTemplate"></TextBlock>
<!--your stuff here-->
</DataTemplate>
<Style x:Key="selectableContentStyle" TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="Required">
<Setter Property="ContentTemplate" Value="{StaticResource requiredTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="Completed">
<Setter Property="ContentTemplate" Value="{StaticResource completedTemplate}" />
</DataTrigger>
<!-- your other Status' here -->
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<ContentControl Width="100" Height="100" Style="{StaticResource selectableContentStyle}"/>
</Grid>
</Window>
I could be wrong, but I believe the DataTemplateSelector is only used when the ItemContainerGenerator creates a container for an item added to the collection. Because a new container isn't generated when a property value changes, a new DataTemplate is never going to be applied via the selector.
As suggested in the comments, I would recommend you look at the VisualStateManager or data triggers, otherwise you're going to have to recreate the container for every item when one or more properties change value.
Just as an extra choice - if you want to stick to your templates, just use s binding with converter.
I came up with a behavior that would theoretically do this.
C#:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
public class UpdateTemplateBehavior : Behavior<ContentPresenter>
{
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(UpdateTemplateBehavior), new FrameworkPropertyMetadata(null, OnContentChanged));
public object Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
static void OnContentChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is UpdateTemplateBehavior behavior)
behavior.Update();
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(object), typeof(UpdateTemplateBehavior), new FrameworkPropertyMetadata(null, OnValueChanged));
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is UpdateTemplateBehavior behavior)
behavior.Update();
}
public UpdateTemplateBehavior() : base() { }
protected override void OnAttached()
{
base.OnAttached();
Update();
}
void Update()
{
if (Content != null)
{
BindingOperations.ClearBinding(AssociatedObject, ContentPresenter.ContentProperty);
AssociatedObject.Content = null;
BindingOperations.SetBinding(AssociatedObject, ContentPresenter.ContentProperty, new Binding() { Path = nameof(Content), Source = this });
}
}
}
XAML:
<ContentPresenter ContentTemplateSelector="{StaticResource MySelector}">
<i:Interaction.Behaviors>
<Behavior:UpdateTemplateBehavior Content="{Binding SomeContent}"
Value="{Binding SomeValue}"/>
</i:Interaction.Behaviors>
</ContentPresenter>
The content is "updated" (by clearing and then resetting the binding) when the content (in this example, "SomeContent") and an arbitrary value (in this example, "SomeValue") is changed, as well as when the behavior is first attached.
An update is not made unless the content is not null (my project-specific requirement). Not updating upon attaching may avoid unintentionally updating twice at once, but if the value is initially null, an update wouldn't occur until the value changes at least once.
Note: In the above example, I am not sure if the behavior has the same data context as the ContentPresenter. I use a helper class that I did not include here for brevity. Keep that in mind when testing...
Related
I wrote a CustomControl that supports direct content. When I nest a TabControl in it, there's no tab item selected on launch. It happens only when using ItemsSource. Anyone know what's wrong?
Sample implementation
In MainWindow.xaml:
<Window.DataContext>
<local:VM/>
</Window.DataContext>
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TabControl ItemsSource="{Binding Data}"/>
<local:CustomControl1 Grid.Column="1">
<TabControl ItemsSource="{Binding Data}" />
</local:CustomControl1>
</Grid>
VM.cs
class VM
{
public List<int> Data { get; set; } = new List<int>{ 1, 2, 3, 4 };
}
CustomControl1:
[ContentProperty("Content")]
public class CustomControl1 : Control
{
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
public FrameworkElement Content
{
get { return (FrameworkElement)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(FrameworkElement), typeof(CustomControl1), new PropertyMetadata(null));
}
In Generic.xaml:
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<ContentPresenter/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
After some time of poking around I've found a solution, but I am not sure of the exact reasons behind this behavior.
I've modified your example and compared the behavior when the TabControl was put inside a ContentControl and a CustomControl1. Predictably, the first tab in case of ContentControl was selected, but in case of CustomControl1 it was not. After inspecting the ContentControl source code (in particular the ContentControl.OnContentChanged method) we can see that what it does is it sets its content as its logical child.
I then confirmed that setting the TabControl as the CustomControl1's logical child does the trick (but I'm not sure why, as I mentioned). So the minimal solution to your problem is to handle the logical relation between your control and its content in the property changed callback, e.g.:
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register(
"Content",
typeof(FrameworkElement),
typeof(CustomControl1),
new PropertyMetadata(null)
{
PropertyChangedCallback = OnContentChanged
});
private static void OnContentChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = (CustomControl1)d;
if (e.OldValue != null)
control.RemoveLogicalChild(e.OldValue);
if (e.NewValue != null)
control.AddLogicalChild(e.NewValue);
}
Note though that this lacks some checks (e.g. if new value already does have a logical parent or the control is a part of a template), so you might want to simply copy the code from the referenced source, or fallback to deriving your control from ContentControl altogether.
I'm trying to implement a hamburger button by myself in a Windows 10 app. I'm running into a little trouble with my ResourceDictionary when trying to set the Command property of a Button (via a style). Here is my code:
Hamburger.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Octopie.Styles.Hamburger"
xmlns:local="using:Octopie.Styles">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Square.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style x:Key="HamburgerStyle" TargetType="Button" BasedOn="{StaticResource SquareStyle}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Command" Value="{Binding OnClicked}"/> <!--This is the part that's having issues-->
<Setter Property="Content" Value=""/>
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
</Style>
</ResourceDictionary>
Hamburger.xaml.cs
namespace Octopie.Styles
{
public sealed partial class Hamburger : ResourceDictionary
{
public Hamburger()
{
this.InitializeComponent();
}
public ICommand OnClicked => new ClickedCommand();
private class ClickedCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) =>
parameter is Button;
public void Execute(object parameter)
{
var button = (Button)parameter;
// Walk up the tree until we reach a SplitView
FrameworkElement parent = button;
do
parent = parent.Parent as FrameworkElement;
while (!(parent is SplitView));
var splitView = (SplitView)parent;
splitView.IsPaneOpen = !splitView.IsPaneOpen;
}
}
}
}
For some reason the binding for the Command property doesn't seem to be working; when I set a breakpoint inside the Execute method and click the button, the breakpoint is never hit. I tried adding a DataContext="{Binding RelativeSource={RelativeSource Self}}" to the top of the XAML file, but for some reason ResourceDictionary doesn't seem to support DataContext.
tl;dr: What can I do to make the Button.Command property bind correctly to OnClicked within the setter?
Like Mike said, usually we won't set Button.Command in ResourceDictionary. A hamburger button may not only be in SplitView but can be in another place and then you may need bind another command. So you can refer to Mike's suggestion.
But if you do want to set it in ResourceDictionary, you can try like following:
Firstly, in your case, your command is fixed, you can declare your ClickedCommand as a public class, then in the Style,set the Command like:
<Setter Property="Command">
<Setter.Value>
<local:ClickedCommand />
</Setter.Value>
</Setter>
After this, you can use your command, but this won't fix your problem as in ClickedCommand, you use parameter to retrieve the Button, but the parameter is not the "sender" of the Command, but the object passed with CommandParameter property. So we need set this in the Style.
However, Bindings in Style Setters are not supported in UWP Apps. See Remarks in Setter class:
The Windows Runtime doesn't support a Binding usage for Setter.Value (the Binding won't evaluate and the Setter has no effect, you won't get errors, but you won't get the desired result either).
A workaround for this is using attached property to set up the binding in code behind for you. For example:
public class BindingHelper
{
public static readonly DependencyProperty CommandParameterBindingProperty =
DependencyProperty.RegisterAttached(
"CommandParameterBinding", typeof(bool), typeof(BindingHelper),
new PropertyMetadata(null, CommandParameterBindingPropertyChanged));
public static bool GetCommandParameterBinding(DependencyObject obj)
{
return (bool)obj.GetValue(CommandParameterBindingProperty);
}
public static void SetCommandParameterBinding(DependencyObject obj, bool value)
{
obj.SetValue(CommandParameterBindingProperty, value);
}
private static void CommandParameterBindingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
BindingOperations.SetBinding(d, Button.CommandParameterProperty, new Binding { RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.Self } });
}
}
}
Then in Style, using
<Setter Property="local:BindingHelper.CommandParameterBinding" Value="True" />
will set the Button as CommandParameter. Your Hamburger.xaml may like:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Octopie.Styles">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Square.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="HamburgerStyle" TargetType="Button" BasedOn="{StaticResource SquareStyle}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Command">
<Setter.Value>
<local:ClickedCommand />
</Setter.Value>
</Setter>
<Setter Property="local:BindingHelper.CommandParameterBinding" Value="True" />
<Setter Property="Content" Value="" />
<Setter Property="FontFamily" Value="Segoe MDL2 Assets" />
</Style>
</ResourceDictionary>
I delete x:Class="Octopie.Styles.Hamburger" and Hamburger.xaml.cs as there is no need to use code-behind for your ResourceDictionary.
Now we can use this ResourceDictionary in our page like:
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Hamburger.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<SplitView DisplayMode="CompactOverlay" IsPaneOpen="True">
<SplitView.Pane>
<StackPanel>
<Button Style="{StaticResource HamburgerStyle}" />
</StackPanel>
</SplitView.Pane>
</SplitView>
</Grid>
But there is another problem in Execute method of ClickedCommand. In this method, you've used FrameworkElement.Parent to retrieve the SplitView. But
Parent can be null if an object was instantiated, but is not
attached to an object that eventually connects to a page object root.
Most of the time, Parent is the same value as returned by
VisualTreeHelper APIs. However, there may be cases where Parent
reports a different parent than VisualTreeHelper does.
And in your case, you need use VisualTreeHelper.GetParent to get the SplitView. We can use a helper method to do this:
public static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
//get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
else
return FindParent<T>(parentObject);
}
Then in Execute method using:
public void Execute(object parameter)
{
var button = (Button)parameter;
var splitView = FindParent<SplitView>(button);
splitView.IsPaneOpen = !splitView.IsPaneOpen;
}
Now the HamburgerStyle will work as you want.
What the hell?
You're going about this all wrong. You don't need to declare a new ICommand in a ResourceDictionary, it simply doesn't belong there. It belongs in your View Model, or whatever the Button.DataContext is set to.
The purpose of a Style is to control the look and feel of your controls, they should not explicitly set their own behaviours (commands).
Let me show you an example. You should declare your button like this:
<Button Style="{StaticResource HamburgerStyle}" Command="{Binding ClickedCommand}"/>
Where ClickedCommand is an object in your View Model.
Your HamburgerStyle should not set it's own Command property, otherwise you are limiting your Button to one single implementation of ICommand, this is unwise.
I've got some troubles with a custom control I need to create. I try to explain you my needs first
I need to have a combobox that permits to check more than one item at time (with checkbox) but I want it to be smart enought to bind to a specific type.
I've found some MultiSelectionComboBox but none reflects my need.
Btw my main problem is that I wish to have a generic class as
public class BaseClass<T> : BaseClass
{
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable<T>), typeof(BaseClass<T>), new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(BaseClass<T>.OnItemsSourceChanged)));
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
int i = 0;
//MultiSelectComboBox control = (MultiSelectComboBox)d;
//control.DisplayInControl();
}
public IEnumerable<T> ItemsSource
{
get { return (IEnumerable<T>)GetValue(ItemsSourceProperty); }
set
{
SetValue(ItemsSourceProperty, value);
}
}
}
public class BaseClass : Control
{
}
and a more context specific item for example
public class MultiCurr : BaseClass<Currency>
{
static MultiCurr()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiCurr), new FrameworkPropertyMetadata(typeof(MultiCurr)));
}
}
In my App.xaml I've defined a resource as
<ResourceDictionary>
<Style TargetType="local:MultiCurr">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiCurr">
<ComboBox Width="120" Background="Red" Height="30" ItemsSource="{Binding ItemsSource}" DisplayMemberPath="Description" ></ComboBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
In my MainWindow I've created an object as
<Grid>
<local:MultiCurr x:Name="test" ItemsSource="{Binding Currencies}"></local:MultiCurr>
</Grid>
and the MainWindow.cs is defined as
public partial class MainWindow : Window, INotifyPropertyChanged
{
private IList currencies;
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
this.Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var lst = new List<Currency>();
for (int i = 0; i < 10; i++)
{
var curr = new Currency
{
ID = i,
Description = string.Format("Currency_{0}", i)
};
lst.Add(curr);
}
Currencies = lst;
}
public IList<Currency> Currencies
{
get
{
return this.currencies;
}
set
{
this.currencies = value;
NotifyPropertyChanged("Currencies");
}
}
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged(String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And here's the result ...
I was wondering what am I doing wrong? is it possible what am I tring to achieve?
Thanks
UPDATE #1:
I've seen that the main problem is the datacontext of the custom usercontrol
<Application.Resources>
<ResourceDictionary>
<Style TargetType="local:MultiCurr">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MultiCurr">
<ComboBox Width="120" Background="Red" Height="30" ItemsSource="{Binding **Currencies**}" DisplayMemberPath="{Binding **DisplayMemeberPath**}" ></ComboBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Application.Resources>
If I put ItemsSource as Currency (which is a property of the MainWindow) it shows.
If I put ItemsSource and DisplayMemberPath (which are defined in the BaseClass no.. how can I set the context of the usercontrol to itself?)
UPDATE #2
I've added a GoogleDrive link to the project here if anyone wants to try the solution
Thanks
Combobox is not suitable control for multiselection, because it has given behaviour, that when yo select item, Combobox closes itself. That's why Combobox doest not have SelectionMode property like ListBox. I think that ListBox inside expander is what you need.
Generic Types are not a way to go. WPF handles this different, better way. Take listbox as an example. If you bind listbox.itemssource to generic observable collection, and you try to define e.g ItemTemplate, you get full intellisense when writing bindings and warning if you bind to not existing property. http://visualstudiomagazine.com/articles/2014/03/01/~/media/ECG/visualstudiomagazine/Images/2014/03/Figure8.ashx WPF designer automatically recognizes type parameter of your observable collection. Of cousre you need to specify type of datacontext in your page by using something like this: d:DataContext="{d:DesignInstance search:AdvancedSearchPageViewModel}". However your control dont have to be and shouldn't be aware of type of items.
Following example demonstrates control that meets your requirements:
<Expander>
<Expander.Header>
<ItemsControl ItemsSource="{Binding ElementName=PART_ListBox, Path=SelectedItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<Run Text="{Binding Mode=OneWay}" />
<Run Text=";" />
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Expander.Header>
<Expander.Content>
<ListBox x:Name="PART_ListBox" SelectionMode="Multiple">
<ListBox.ItemsSource>
<x:Array Type="system:String">
<system:String>ABC</system:String>
<system:String>DEF</system:String>
<system:String>GHI</system:String>
<system:String>JKL</system:String>
</x:Array>
</ListBox.ItemsSource>
</ListBox>
</Expander.Content>
</Expander>
I reccomend you to create control derived from ListBox (not usercontrol).
I have hardcoded datatemplates, but you should expose them in your custom dependency properties and use TemplateBinding in you control template. Of course you need to modify expander so it looks like combobox and ListBoxItem style so it looks like CheckBox, but it is ease.
I am having an issue when using a DataTrigger to manipulate the IsEnabled property of a control. Normally it works fine, however when I initialize the IsEnabled state within the View's Initialized event, the dynamic stylizing no longer works.
Here's my code. I trimmed it down to the simplest example I could.
Why is this occurring, and what can I do to allow me to set IsEnabled both by a style trigger and by initializing it in the code behind?
Thanks in advance!
View:
(Contains a textbox that should be enabled/disabled depending on the value of a checkbox)
<Window x:Class="IsEnabled.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Initialized="Window_Initialized">
<StackPanel Orientation="Vertical">
<TextBox x:Name="txtTarget" Width="200">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=ToggleValue}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<CheckBox x:Name="chkSource" IsChecked="{Binding Path=ToggleValue}" />
</StackPanel>
</Window>
View Codebehind:
(The only addition is the implementation of the Initialized event setting the inital state for IsEnabled)
using System;
using System.Windows;
namespace IsEnabled.Views
{
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
}
private void Window_Initialized(object sender, EventArgs e)
{
txtTarget.IsEnabled = false;
}
}
}
ViewModel:
(ViewModelBase holds the implementation of the INotifyPropertyChanged interface)
using System;
namespace IsEnabled.ViewModels
{
class MainViewModel : ViewModelBase
{
private bool _ToggleValue;
public bool ToggleValue
{
get { return _ToggleValue; }
set
{
_ToggleValue = value;
OnPropertyChanged(this, "ToggleValue");
}
}
}
}
Have a look at dependency property value precedence, and how changing values from different places, Styles, Triggers, Animations etc. work together.
Add to your Binding Mode=TwoWay and it should work.
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}">