I tried to apply new style for my custom controls by merging the style resources to the application resources while adding the control to UI, but new styles are not applied to the controls for the first time.
Sample control
CustomTextBoxExt.cs
public class CustomTextBoxExt : TextBox
{
static CustomTextBoxExt()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomTextBoxExt), new FrameworkPropertyMetadata(typeof(CustomTextBoxExt)));
}
}
default style
Generic.xaml
<Style x:Key="TextBoxExtStyle" TargetType="{x:Type local:CustomTextBoxExt}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" />
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="AllowDrop" Value="True" />
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomTextBoxExt}">
<Border
x:Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<ScrollViewer
x:Name="PART_ContentHost"
Focusable="False"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Opacity" Value="0.56" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="#FF7EB4EA" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter TargetName="border" Property="BorderBrush" Value="#FF569DE5" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsInactiveSelectionHighlightEnabled" Value="True" />
<Condition Property="IsSelectionActive" Value="False" />
</MultiTrigger.Conditions>
<Setter Property="SelectionBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}" />
</MultiTrigger>
</Style.Triggers>
</Style>
<Style BasedOn="{StaticResource TextBoxExtStyle}" TargetType="{x:Type local:CustomTextBoxExt}" />
Custom theme
TextBoxExtStyle.xaml
<Style x:Key="MaterialTextBoxExtStyle" TargetType="{x:Type local:CustomTextBoxExt}">
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="Foreground" Value="#DD000000" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#FF9E9E9E" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="SelectionBrush" Value="#FF0279FF" />
<Setter Property="AllowDrop" Value="true" />
<Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="AllowDrop" Value="true" />
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst" />
<Setter Property="Stylus.IsFlicksEnabled" Value="False" />
<Setter Property="CaretBrush" Value="#DD000000" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomTextBoxExt}">
<Grid>
<Border
x:Name="border"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="true">
<ScrollViewer
x:Name="PART_ContentHost"
Background="Transparent"
Focusable="False"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden" />
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="border" Property="BorderBrush" Value="#FF757575" />
<Setter TargetName="border" Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="#DD000000" />
<Setter Property="CaretBrush" Value="#DD000000" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="true">
<Setter TargetName="border" Property="BorderBrush" Value="#FF0279FF" />
<Setter Property="BorderThickness" Value="0,0,0,2" />
<Setter Property="Padding" Value="0,0,0,-1" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="border" Property="Background" Value="Transparent" />
<Setter TargetName="border" Property="BorderBrush" Value="#FFE0E0E0" />
<Setter Property="Foreground" Value="#60000000" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style BasedOn="{StaticResource MaterialTextBoxExtStyle}" TargetType="{x:Type local:CustomTextBoxExt}" />
and using attached property, trying to change style from default style,
public class SkinExt
{
public static string GetTheme(DependencyObject obj)
{
return (string)obj.GetValue(ThemeProperty);
}
public static void SetTheme(DependencyObject obj, string value)
{
obj.SetValue(ThemeProperty, value);
}
// Using a DependencyProperty as the backing store for Theme. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ThemeProperty =
DependencyProperty.RegisterAttached("Theme", typeof(string), typeof(SkinExt), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.Inherits, new PropertyChangedCallback(OnVisualStyleChanged)));
private static void OnVisualStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != e.OldValue && !string.IsNullOrEmpty(e.NewValue.ToString()) /*&& d is FrameworkElement && (d as FrameworkElement).IsLoaded*/)
{
SkinExt.ApplyTheme(d, e.NewValue.ToString());
}
}
internal static void ApplyTheme(DependencyObject obj, string style)
{
Type itemType = obj.GetType();
List<string> styles = GetDictionaries(obj.GetType().Name.ToString(), style);
if (styles != null && styles.Count > 0)
{
foreach (var path in styles)
{
var rdict = new ResourceDictionary() { Source = new Uri(path, UriKind.RelativeOrAbsolute) };
bool alreadyExist = false;
foreach (var dictionaryFiles in Application.Current.Resources.MergedDictionaries)
{
if (dictionaryFiles.Source.OriginalString.Contains(path))
{
alreadyExist = true;
break;
}
}
if (!alreadyExist)
{
Application.Current.Resources.MergedDictionaries.Add(rdict);
Console.WriteLine(path);
}
}
}
}
internal static List<string> GetDictionaries(String type, string style)
{
List<string> styles = new List<string>();
#region Switch
switch (type)
{
case "CustomTextBoxExt":
styles.Add("/TextBoxExt;component/TextBoxExt/TextBoxExtStyle.xaml");
break;
case "ButtonExt":
styles.Add("/TextBoxExt;component/ButtonExt/ButtonExtStyle.xaml");
break;
case "Label":
styles.Add("/TextBoxExt;component/LabelStyle.xaml");
break;
}
# endregion
return styles;
}
}
setting
local:SkinExt.Theme="Material"
in mainwindow/grid works as expected when children are added directly. But, when using below lazyextension style is not working.
public static class LazyLoadExtensions
{
public static LazyUIElementCollection GetLazyChildrens(DependencyObject obj)
{
return (LazyUIElementCollection)obj.GetValue(LazyChildrensProperty);
}
public static void SetLazyChildrens(DependencyObject obj, LazyUIElementCollection value)
{
obj.SetValue(LazyChildrensProperty, value);
}
public static readonly DependencyProperty LazyChildrensProperty =
DependencyProperty.RegisterAttached("LazyChildrens", typeof(LazyUIElementCollection), typeof(LazyLoadExtensions), new PropertyMetadata(OnLazyChildrensChanged));
private static void OnLazyChildrensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var wrapPanel = d as WrapPanel;
var childrens = LazyLoadExtensions.GetLazyChildrens(wrapPanel);
for (int i = 0; i < childrens.Count; i++)
{
var child = childrens[i];
wrapPanel.Children.Add(child);
}
}
}
public class LazyUIElementCollection : List<UIElement>
{
public LazyUIElementCollection()
{
}
}
Works
<Grid local:SkinExt.Theme="Material">
<WrapPanel x:Name="wrapPanel">
<!--<local:LazyLoadExtensions.LazyChildrens>-->
<!--<local:LazyUIElementCollection>-->
<StackPanel Margin="10">
<TextBlock Margin="0,0,0,8" Text="MS Label" />
<Label
Width="200"
Height="25"
Content="Material" />
</StackPanel>
<StackPanel Margin="10">
<TextBlock Margin="0,0,0,8" Text="Custom TextBox" />
<local:CustomTextBoxExt
Width="200"
Height="25"
Text="Material" />
</StackPanel>
<!--</local:LazyUIElementCollection>-->
<!--</local:LazyLoadExtensions.LazyChildrens>-->
</WrapPanel>
</Grid>
Not Working
<Grid local:SkinExt.Theme="Material">
<WrapPanel x:Name="wrapPanel">
<local:LazyLoadExtensions.LazyChildrens>
<local:LazyUIElementCollection>
<StackPanel Margin="10">
<TextBlock Margin="0,0,0,8" Text="MS Label" />
<Label
Width="200"
Height="25"
Content="Material" />
</StackPanel>
<StackPanel Margin="10">
<TextBlock Margin="0,0,0,8" Text="Custom TextBox" />
<local:CustomTextBoxExt
Width="200"
Height="25"
Text="Material" />
</StackPanel>
</local:LazyUIElementCollection>
</local:LazyLoadExtensions.LazyChildrens>
</WrapPanel>
</Grid>
Works for second item
Style applying correctly for second customtextboxext
<Grid local:SkinExt.Theme="Material">
<WrapPanel x:Name="wrapPanel">
<local:LazyLoadExtensions.LazyChildrens>
<local:LazyUIElementCollection>
<StackPanel Margin="10">
<TextBlock Margin="0,0,0,8" Text="MS Label" />
<Label
Width="200"
Height="25"
Content="Material" />
</StackPanel>
<StackPanel Margin="10">
<TextBlock Margin="0,0,0,8" Text="Custom TextBox" />
<local:CustomTextBoxExt
Width="200"
Height="25"
Text="Material" />
<local:CustomTextBoxExt
Width="200"
Height="25"
Text="Material" />
</StackPanel>
</local:LazyUIElementCollection>
</local:LazyLoadExtensions.LazyChildrens>
</WrapPanel>
</Grid>
Reproducible sample : https://drive.google.com/open?id=1iB9sY90T7aRaaRTzVc1EvE2qFU13fHG7
Check the above sample and let me know your ideas
cool project !
I'm not exactly sure why the problem occurs but it seems like a timing problem.
I found the following workaround:
In LazyLoadExtensions:
private static void OnLazyChildrensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var wrapPanel = d as WrapPanel;
var childrens = LazyLoadExtensions.GetLazyChildrens(wrapPanel);
for (int i = 0; i < childrens.Count; i++)
{
var child = childrens[i];
//The dictionaries containing the styles are added
SkinExt.SetTheme(child, SkinExt.GetTheme(wrapPanel));
wrapPanel.Children.Add(child);
}
}
This way the dictionaries containing the style are merged before the the children are added and the styles are correctly applied.
Edit:
Since you can't change the LazyLoadExtensions class:
In SkinExt class:
internal static void ApplyTheme(DependencyObject obj, string style)
{
Type itemType = obj.GetType();
List<string> styles = GetDictionaries(obj.GetType().Name.ToString(), style);
if (styles != null && styles.Count > 0)
{
foreach (var path in styles)
{
var rdict = new ResourceDictionary() { Source = new Uri(path, UriKind.RelativeOrAbsolute) };
bool alreadyExist = false;
foreach (var dictionaryFiles in Application.Current.Resources.MergedDictionaries)
{
if (dictionaryFiles.Source.OriginalString.Contains(path))
{
alreadyExist = true;
break;
}
}
if (!alreadyExist)
{
Application.Current.Resources.MergedDictionaries.Add(rdict);
if (obj is FrameworkElement frameworkElement && frameworkElement.IsInitialized)
{
//The style won't be applied automaticaly
frameworkElement.Style = rdict.Values.OfType<Style>().First(s => s.TargetType == itemType);
}
else
{
//Nothing to do, style will be applied automaticaly
}
Console.WriteLine(path);
}
}
}
}
If the IsInitialized property is true, the style from the added ressourceDictionnary won't be applied, you can check it in debugger.
https://learn.microsoft.com/en-gb/dotnet/api/system.windows.frameworkelement.isinitialized?view=netcore-3.1#System_Windows_FrameworkElement_IsInitialized
If it's helpful, applying theme inside dispatcher works as expected ,
d.Dispatcher.BeginInvoke(new Action(() =>
{
SkinExt.ApplyTheme(d, e.NewValue.ToString());
}));
Related
I have a listbox of elements, and I set the ItemPanel as a WrapPanel as I want my Panel to wrap every 4 elements.
I used the following:
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
Now, I want thay my items will be displayed as follows:
When only 4 Items or less:
When more than 4 Items:
I need that the cornerRadius will be applied on the four edges whether it's only one row or not.
This turned out to be a bit painful. If you want the corner radius and the border thickness to be properly parameterized, that'll take more work: You'd need value converters to create or modify CornerRadius and Thickness values as needed.
Another approach would have been to omit the triggers and write two big multiconverters, for Thickness and CornerRadius, that take the same parameters as the one I wrote, plus the "default" border thickness and corner radius values, and then return Thickness and CornerRadius respectively.
<Style TargetType="ListBox" x:Key="GridLineListBox">
<Style.Resources>
<local:CellTypeConverter x:Key="CellTypeConverter" />
</Style.Resources>
<Setter Property="AlternationCount" Value="{x:Static sys:Int32.MaxValue}" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="UseLayoutRounding" Value="True" />
<Setter Property="BorderBrush" Value="SteelBlue" />
<Setter Property="local:GridLineListBox.ColumnCount" Value="6" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Border
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="16"
ClipToBounds="True"
>
<ItemsPresenter Margin="-1" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<UniformGrid
Columns="{Binding (local:GridLineListBox.ColumnCount), RelativeSource={RelativeSource AncestorType=ListBox}}"
IsItemsHost="True"
/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="ListBoxItem" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<!-- Put this in an attached property so we don't have to copy/paste the whole binding for each trigger -->
<Setter Property="local:GridLineListBox.CellType">
<Setter.Value>
<MultiBinding Converter="{StaticResource CellTypeConverter}">
<Binding Path="Items.Count" RelativeSource="{RelativeSource AncestorType=ListBox}" />
<Binding Path="(ItemsControl.AlternationIndex)" RelativeSource="{RelativeSource Self}" />
<Binding Path="(local:GridLineListBox.ColumnCount)" RelativeSource="{RelativeSource AncestorType=ListBox}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Margin" Value="0" />
<Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=ListBox}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<!--
Negative right/bottom margin because I'm getting a gap with
SnapToDevicePixels and I'm too lazy to figure out the real reason.
-->
<Border
x:Name="Bd"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="0,0,2,2"
Background="{TemplateBinding Background}"
ClipToBounds="True"
Margin="-1"
>
<ContentPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="local:GridLineListBox.CellType" Value="TopLeft">
<Setter TargetName="Bd" Property="CornerRadius" Value="16,0,0,0" />
</Trigger>
<Trigger Property="local:GridLineListBox.CellType" Value="TopRight">
<Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,2" />
<Setter TargetName="Bd" Property="CornerRadius" Value="0,16,0,0" />
</Trigger>
<Trigger Property="local:GridLineListBox.CellType" Value="Right">
<Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,2" />
</Trigger>
<Trigger Property="local:GridLineListBox.CellType" Value="BottomRight">
<Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,0" />
<Setter TargetName="Bd" Property="CornerRadius" Value="0,0,16,0" />
</Trigger>
<Trigger Property="local:GridLineListBox.CellType" Value="Bottom">
<Setter TargetName="Bd" Property="BorderThickness" Value="0,0,2,0" />
</Trigger>
<Trigger Property="local:GridLineListBox.CellType" Value="BottomLeft">
<Setter TargetName="Bd" Property="BorderThickness" Value="0,0,2,0" />
<Setter TargetName="Bd" Property="CornerRadius" Value="0,0,0,16" />
</Trigger>
<Trigger Property="local:GridLineListBox.CellType" Value="SingleRowLeft">
<Setter TargetName="Bd" Property="BorderThickness" Value="0,0,2,0" />
<Setter TargetName="Bd" Property="CornerRadius" Value="16,0,0,16" />
</Trigger>
<Trigger Property="local:GridLineListBox.CellType" Value="SingleRowRight">
<Setter TargetName="Bd" Property="BorderThickness" Value="0,0,0,0" />
<Setter TargetName="Bd" Property="CornerRadius" Value="0,16,16,0" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.MouseOver.Background}"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Selector.IsSelectionActive" Value="False"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Background}"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Selector.IsSelectionActive" Value="True"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Background}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
</Style>
C#
public enum CellType {
TopLeft, Top, TopRight, Right, BottomRight, Bottom, BottomLeft, Left,
SingleRowLeft, SingleRowRight, Inner
}
public class CellTypeConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
var itemsCount = System.Convert.ToInt32(values[0]);
var itemIndex = System.Convert.ToInt32(values[1]);
var columnCount = System.Convert.ToInt32(values[2]);
int rowCount = itemsCount / columnCount;
if (itemsCount % columnCount > 0)
++rowCount;
int lowerRightIndex = (rowCount * columnCount) - 1;
int lowerLeftIndex = (rowCount - 1) * columnCount;
if (itemIndex == 0)
{
return (rowCount == 1) ? CellType.SingleRowLeft : CellType.TopLeft;
}
else if (itemIndex == columnCount - 1)
{
return (rowCount == 1) ? CellType.SingleRowRight : CellType.TopRight;
}
else if (itemIndex < columnCount)
return CellType.Top;
else if (itemIndex == lowerRightIndex)
return CellType.BottomRight;
else if ((itemIndex + 1) % columnCount == 0)
return CellType.Right;
else if (itemIndex == lowerLeftIndex)
return CellType.BottomLeft;
else if (itemIndex > lowerLeftIndex)
return CellType.Bottom;
else if (itemIndex % columnCount == 0)
return CellType.Left;
return CellType.Inner;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public static class GridLineListBox
{
public static CellType GetCellType(ListBoxItem obj)
{
return (CellType)obj.GetValue(CellTypeProperty);
}
public static void SetCellType(ListBoxItem obj, CellType value)
{
obj.SetValue(CellTypeProperty, value);
}
public static readonly DependencyProperty CellTypeProperty =
DependencyProperty.RegisterAttached("CellType", typeof(CellType), typeof(GridLineListBox),
new PropertyMetadata((CellType)(-1)));
public static int GetColumnCount(ListBox obj)
{
return (int)obj.GetValue(ColumnCountProperty);
}
public static void SetColumnCount(ListBox obj, int value)
{
obj.SetValue(ColumnCountProperty, value);
}
public static readonly DependencyProperty ColumnCountProperty =
DependencyProperty.RegisterAttached("ColumnCount", typeof(int), typeof(GridLineListBox),
new PropertyMetadata(0, ColumnCount_PropertyChanged));
private static void ColumnCount_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var target = d as ListBox;
}
}
Example:
<ListBox
ItemsSource="{Binding CollectionOfStrings}"
Style="{StaticResource GridLineListBox}"
>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<Label
Content="{Binding}"
HorizontalAlignment="Center"
/>
<Label
Content="{Binding (local:GridLineListBox.CellType), RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
HorizontalAlignment="Center"
/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
May be I'm missing something, but I think there should be a nice way to add items to ListBoxes whose contents are bound to a data source, ether than having separate button near that ListBox to trigger add_new_item() from the code.
I guess I can figure out how to add a delete button near currently selected button through style. I'm not yet sure which parameter will trigger this. But I'm more concerned with adding items like shown in the image
My Awesome Custom Listbox
I'm not yet advanced into styling. What I do is, I take somebody's style and take it apart, then create what I actually need. I don't even know where to look at to see all styling techniques like looking at a class reference.
I had an idea of adding a button hanging in a corner of a ListBox or ComboBox, but how should I declare a new Event for it so I could assign unique functions to then other than one for everything.
I'm not asking for a complete solution, just a hint of what to do. I'll post an answer when I'll figure it out.
And after a year I've figured these things out.
Just bind to an ObservableCollection and add the value
If that is not what you are looking for please ask a more specific question
You have to create the button yourself and assign a command (MVVM) or click (code behind) to it. Your listbox itemssource should be observablecollection which will notify the UI anytime you add or remove an item from the listbox. If you know that you want a listbox with a button in many places, then you can create a usercontrol out of these two controls. If you want a button inside the lixtbox or combobox then you have to modify the template of these controls, slightly more complicated.
With time I figured these things (UVMCommand is my ICommand interface implementation, it is easier for me to use it this way):
public class Game : Notifiable {
public Game() {
Players = new ObservableCollection<Player>();
AddNewPlayer = new UVMCommand("AddNewPlayer", p => {
var pl = new Player() { Text = "New Player", IsSelected = true, IsEdited = true };
pl.RemoveThis = new UVMCommand("RemoveThis", pp => Players.Remove(pl));
Players.Add(pl);
});
}
private ObservableCollection<Player> _players;
public ObservableCollection<Player> Players { get { return _players; } set { _players = value; OnPropertyChanged("Players"); } }
private UVMCommand _addNewPlayer;
public UVMCommand AddNewPlayer { get { return _addNewPlayer; } set { _addNewPlayer = value; OnPropertyChanged("AddNewPlayer"); } }
}
public class Player : Notifiable {
public Player() {}
private UVMCommand _removeThis;
public UVMCommand RemoveThis { get { return _removeThis; } set { _removeThis = value; OnPropertyChanged("RemoveThis"); } }
}
public class Notifiable : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName) {
// take a copy to prevent thread issues
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And then making a user control in code-behind declare:
public partial class EditableListBox : System.Windows.Controls.ListBox, INotifyPropertyChanged {
public EditableListBox() {
InitializeComponent();
//var s = FindResource("EditableListBoxStyle") as Style;
//Style = s;
}
[Category("Common")]
public UVMCommand AddItem {
get { return (UVMCommand)GetValue(AddItemProperty); }
set {
SetValue(AddItemProperty, value);
}
}
// Using a DependencyProperty as the backing store for AddItem. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AddItemProperty =
DependencyProperty.Register("AddItem", typeof(UVMCommand), typeof(EditableListBox),
new PropertyMetadata(
new UVMCommand("Default Command", p => { Debug.WriteLine("EditableListBox.AddITem not defined"); })));
[Category("Layout")]
public Orientation Orientation {
get { return (Orientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
// Using a DependencyProperty as the backing store for Orientation. This enables animation, styling, binding, etc...
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register("Orientation", typeof(Orientation), typeof(EditableListBox), new PropertyMetadata(Orientation.Vertical));
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName) {
// take a copy to prevent thread issues
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
And in XAML I define a Style with an add button in a template at the end of a list and a delete button in an item template:
<ListBox x:Name="listBox" x:Class="MTCQuest.CustomControls.EditableListBox"
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:MTCQuest"
xmlns:zq="clr-namespace:MTCQuest.ViewModel.zQuest;assembly=MTCQuest.ViewModel"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
xmlns:ccon="clr-namespace:MTCQuest.CustomControls"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Style="{DynamicResource EditableListBoxStyle}"
ItemContainerStyle="{DynamicResource EditableListBoxItemStyle}" d:DataContext="{DynamicResource TestTheme}" ItemsSource="{Binding Questions}">
<ListBox.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources/StyleRes.xaml"/>
<ResourceDictionary Source="../Resources/QuestSpacificControlStyles.xaml"/>
</ResourceDictionary.MergedDictionaries>
<ccon:BindingExists x:Key="BindingExists"/>
<ccon:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter"/>
<zq:Theme x:Key="TestTheme">
<zq:Theme.Questions>
<zq:Question IsEdited="True" IsSelected="True" Text="Some Question" Color="#FF2E00FF"/>
<zq:Question Text="Another Question"/>
</zq:Theme.Questions>
</zq:Theme>
<Style x:Key="EditableListBoxStyle" TargetType="{x:Type ListBox}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource Pallete.Divider}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Foreground" Value="{StaticResource Pallete.PrimaryText}"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="True"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="ItemsPanel" Value="{DynamicResource OrientedItemsPanelTemplate}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBox}">
<ScrollViewer Background="{TemplateBinding Background}" SnapsToDevicePixels="true" Focusable="false" Padding="{TemplateBinding Padding}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<StackPanel Orientation="{Binding Orientation, ElementName=listBox}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}">
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
<ccon:zButton x:Name="AddButton" Command="{Binding AddItem, RelativeSource={RelativeSource TemplatedParent}}"
HorizontalContentAlignment="Left"
Background="Transparent" Foreground="{DynamicResource Pallete.PrimaryText}"
BorderThickness="0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<Rectangle Width="16" Height="16" Margin="5,0"
Fill="{DynamicResource Pallete.Accent}"
OpacityMask="{DynamicResource Icon_PlusSign}"/>
<TextBlock Text="Add" Foreground="{StaticResource Pallete.PrimaryText}"/>
</StackPanel>
</ccon:zButton>
</StackPanel>
</ScrollViewer>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="AddButton" Property="Visibility" Value="Visible"/>
</Trigger>
<DataTrigger Binding="{Binding}" Value="{x:Null}">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Effect">
<Setter.Value>
<ccon:DesaturateEffect DesaturationFactor=".25"/>
</Setter.Value>
</Setter>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsGrouping" Value="true"/>
<Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="EditableListBoxItemStyle" TargetType="{x:Type ListBoxItem}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="5,0"/>
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
<Setter Property="FocusVisualStyle">
<Setter.Value>
<Style>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Margin="2" SnapsToDevicePixels="True" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<Grid Height="33" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="TB" Text="{Binding Text}"
VerticalContentAlignment="Center"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Padding="5, 0" BorderThickness="0" GotFocus="TB_GotFocus"
Visibility="Collapsed"/>
<Label x:Name="Lb" Content="{Binding Text}"
VerticalContentAlignment="Center"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Padding="5, 0" BorderThickness="0" Margin="2,1,0,0" />
<xctk:ColorPicker x:Name="CB" Grid.Column="1" Width="48" Visibility="Collapsed"
SelectedColor="{Binding Color}"
ShowRecentColors="True" ShowDropDownButton="False" ShowStandardColors="False"/>
<!--<ccon:zButton x:Name="CB" OpacityMask="{DynamicResource Icon_Edit}"
Width="16" Height="16" Panel.ZIndex="19"
Background="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}" BorderThickness="0"
Visibility="Collapsed" Margin="2" Grid.Column="1"/>-->
<ccon:zButton x:Name="DB" OpacityMask="{DynamicResource Icon_MinusSign}"
Command="{Binding RemoveThis}"
Width="16" Height="16" Background="#FFD12929" BorderThickness="0"
Visibility="Collapsed" Margin="2" Grid.Column="2"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<!--<DataTrigger Binding="{Binding IsEdited}" Value="true">
<Setter Property="FocusManager.FocusedElement" Value="{Binding ElementName=TB}" />
</DataTrigger>-->
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="#1F26A0DA"/>
<Setter Property="BorderBrush" TargetName="Bd" Value="#A826A0DA"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Selector.IsSelectionActive" Value="False"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="#3DDADADA"/>
<Setter Property="BorderBrush" TargetName="Bd" Value="#FFDADADA"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Selector.IsSelectionActive" Value="True"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="#3D26A0DA"/>
<Setter Property="BorderBrush" TargetName="Bd" Value="#FF26A0DA"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Visibility" TargetName="Lb" Value="Collapsed"/>
<Setter Property="Visibility" TargetName="TB" Value="Visible"/>
<Setter Property="Visibility" TargetName="DB" Value="Visible"/>
<Setter Property="Visibility" TargetName="CB" Value="Visible"/>
</MultiTrigger>
<DataTrigger Binding="{Binding Color, Converter={StaticResource BindingExists}, FallbackValue=false}" Value="false">
<Setter Property="Visibility" TargetName="CB" Value="Collapsed"/>
</DataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ItemsPanelTemplate x:Key="OrientedItemsPanelTemplate">
<VirtualizingStackPanel IsItemsHost="True"
Orientation="{Binding Orientation, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ccon:EditableListBox}}}"/>
</ItemsPanelTemplate>
</ResourceDictionary>
</ListBox.Resources>
And now it's just a:
<ccon:EditableListBox ItemsSource="{Binding Players}" AddItem="{Binding AddNewPlayer}" Orientation="Horizontal" HorizontalContentAlignment="Center" ScrollViewer.VerticalScrollBarVisibility="Disabled"/>
And Hell Yeagh! I can make it either vertical or horizontal.
I can't belive that when I was just learning WPF I've tried to do something that takes so many things to know and be able to write them (Commands, Templates, Triggers, DependencyProperties, INotifyPropertyChanged and many more), that I had no idea even existed. Just a yaer ago :)
These triggers work like expected when this custom control inherits from Control, but not when it inherits from button. When it inherits from button, the second trigger is never triggered. Also, when inherited from Button, the control will remain in the hovered state (i.e. first trigger is satisfied) if the mouse button remains held down, even if the cursor moves out of the control.
Snippet from Generic.xaml:
<Style TargetType="{x:Type ui:SquareButton}">
<Setter Property="Background" Value="{StaticResource BackgroundColorBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource MainColorBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="0,2,2,0" />
<Setter Property="Padding" Value="2" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:SquareButton}">
<Border x:Name="PART_Border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}" HorizontalAlignment="Center" CornerRadius="{TemplateBinding CornerRadius}">
<Grid Height="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=AHeight}" Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Height}">
<ui:ColorableImage x:Name="PART_Image" Source="calendar-black.png" Color="Black" Stretch="None"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource SecondaryColorBrush}" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True" />
<Condition Property="IsMouseLeftButtonDown" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="Background" Value="{StaticResource MainColorBrush}" />
<Setter TargetName="PART_Image" Property="Color" Value="{StaticResource BackgroundColor}" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
SquareButton.cs:
public class SquareButton : Button
{
static SquareButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SquareButton), new FrameworkPropertyMetadata(typeof(SquareButton)));
}
private static DependencyPropertyKey IsMouseLeftButtonDownPropertyKey = DependencyProperty.RegisterReadOnly("IsMouseLeftButtonDown", typeof(bool), typeof(SquareButton), , new PropertyMetadata());
public static DependencyProperty IsMouseLeftButtonDownProperty = IsMouseLeftButtonDownPropertyKey.DependencyProperty;
public bool IsMouseLeftButtonDown
{
get { return (bool)GetValue(IsMouseLeftButtonDownProperty); }
private set { SetValue(IsMouseLeftButtonDownPropertyKey, value); }
}
public static DependencyProperty CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(SquareButton));
public CornerRadius CornerRadius
{
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Border border = GetTemplateChild("PART_Border") as Border;
if (border != null)
{
border.MouseLeftButtonDown += new MouseButtonEventHandler(Border_MouseLeftButtonDown);
border.MouseLeftButtonUp += new MouseButtonEventHandler(Border_MouseLeftButtonUp);
border.MouseLeave += new MouseEventHandler(Border_MouseLeave);
}
}
private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
IsMouseLeftButtonDown = true;
}
private void Border_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
IsMouseLeftButtonDown = false;
}
private void Border_MouseLeave(object sender, MouseEventArgs e)
{
IsMouseLeftButtonDown = false;
}
}
You can use a trigger on the IsPressed property instead of your multitrigger :
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="{StaticResource MainColorBrush}" />
<Setter TargetName="PART_Image" Property="Color" Value="{StaticResource BackgroundColor}" />
</Trigger>
My Tab control has a resource like this:
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Border" Property="Background" Value="White" />
<Setter TargetName="Image" Property="Visibility" Value="Visible" />
<Setter TargetName="Border" Property="BorderBrush" Value="Gray" />
<Setter TargetName="TextBlock" Property="Foreground" Value="Black" />
<Setter TargetName="Border" Property="Margin" Value="-2,0,2,-1" />
</Trigger>
<Trigger Property="IsMouseOver" Value="True" SourceName="Border" >
<Setter TargetName="Border" Property="Background" Value="White" />
<Setter TargetName="Border" Property="BorderBrush" Value="DarkGray" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Grid Background="WhiteSmoke" Name="Grid1">
<Grid.RowDefinitions>
<RowDefinition Height="5"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Canvas Background="{StaticResource {x:Static SystemColors.ActiveCaptionBrushKey}}" />
</Grid>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
How can I add a control like "Label" to Grid(in Resource with name of "Grid1") of my TabControl?
Try to search the grid in the VisualTree and add then your control to it. You can use the following helper function to find the grids and check then for the name. Otherwise you can also extend the code for a easier search (by adding a name parameter).
void FindChildFrameworkElementsOfType<T>(DependencyObject parent,IList<T> list) where T: FrameworkElement{
DependencyObject child;
for(int i=0;i< VisualTreeHelper.GetChildrenCount(parent);i++){
child = VisualTreeHelper.GetChild(parent, i);
if (child is T) {
list.Add((T)child);
}
FindChildFrameworkElementsOfType<T>(child,list);
}
}
Something like:
List<Grid> list=new List<Grid>();
FindCHildFrameworkElementsOfType<Grid>(this,list)
foreach(Grid grid in list){
if(grid.Name=="Grid1"){
// Add here your control
break;
}
}
You can add a Loaded event handler:
<Grid Background="WhiteSmoke" Name="Grid1" Loaded="Grid1_Loaded">
and populate and/or record the reference to the grid in the handler:
private Grid grid1;
private void grid1_Loaded(object sender, RoutedEventArgs e)
{
grid1 = sender as Grid;
// add label, etc.
}
Special case:
event handlers in a resource dictionary
After a lot of searching, I have not found any solution for the following problem.
I need a treeview control with "checkboxed" treeview items and the CheckedItems property for convenient data binding (for example, treeview of folders' structure, when user checks folders, the size of checked folders is displayed in a textbox).
By the way, I have read the article «Working with Checkboxes in the WPF TreeView», Josh Smith, but the "IsChecked" approach is not appropriate in my case because I need to bind CheckedItems as a collection.
I would appreciate any help!
The image link has been attached. I want the listbox to be data bound to CheckedItems property of CheckTreeView. Does anybody know how to implement the generic CheckTreeView with possible binding to CheckedItems collection?
Update
Finally got around to update the CheckBoxTreeView with the missing features. The CheckBoxTreeViewLibrary source can be downloaded here
Adds the CheckedItems property
CheckedItems is an ObservableCollection<T> where T is the interal type of ItemsSource
CheckedItems supports two-way Binding to source
If a CheckBoxTreeViewItem hasn't been generated yet (not expanded to) then the source for it won't be in the CheckedItems collection until it has been generated
The Control can be used just like a regular TreeView. To add two-way binding for the IsChecked property, the CheckBoxTreeViewItemStyle.xaml ResourceDictionary must be merged. e.g.
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/CheckBoxTreeViewLibrary;component/Themes/CheckBoxTreeViewItemStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
Then the ItemContainerStyle can be used like this
<cbt:CheckBoxTreeView ...>
<cbt:CheckBoxTreeView.ItemContainerStyle>
<Style TargetType="{x:Type cbt:CheckBoxTreeViewItem}"
BasedOn="{StaticResource {x:Type cbt:CheckBoxTreeViewItem}}">
<Setter Property="IsChecked" Value="{Binding IsChecked}"/>
<!-- additional Setters, Triggers etc. -->
</Style>
</cbt:CheckBoxTreeView.ItemContainerStyle>
</cbt:CheckBoxTreeView>
CheckBoxTreeView.cs
namespace CheckBoxTreeViewLibrary
{
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(CheckBoxTreeViewItem))]
public class CheckBoxTreeView : TreeView
{
public static DependencyProperty CheckedItemsProperty =
DependencyProperty.Register("CheckedItems",
typeof(IList),
typeof(CheckBoxTreeView));
private RoutedEventHandler Checked_EventHandler;
private RoutedEventHandler Unchecked_EventHandler;
public CheckBoxTreeView()
: base()
{
Checked_EventHandler = new RoutedEventHandler(checkBoxTreeViewItem_Checked);
Unchecked_EventHandler = new RoutedEventHandler(checkBoxTreeViewItem_Unchecked);
DependencyPropertyDescriptor dpd =
DependencyPropertyDescriptor.FromProperty(CheckBoxTreeView.ItemsSourceProperty, typeof(CheckBoxTreeView));
if (dpd != null)
{
dpd.AddValueChanged(this, ItemsSourceChanged);
}
}
void ItemsSourceChanged(object sender, EventArgs e)
{
Type type = ItemsSource.GetType();
if (ItemsSource is IList)
{
Type listType = typeof(ObservableCollection<>).MakeGenericType(type.GetGenericArguments()[0]);
CheckedItems = (IList)Activator.CreateInstance(listType);
}
}
internal void OnNewContainer(CheckBoxTreeViewItem newContainer)
{
newContainer.Checked -= Checked_EventHandler;
newContainer.Unchecked -= Unchecked_EventHandler;
newContainer.Checked += Checked_EventHandler;
newContainer.Unchecked += Unchecked_EventHandler;
}
protected override DependencyObject GetContainerForItemOverride()
{
CheckBoxTreeViewItem checkBoxTreeViewItem = new CheckBoxTreeViewItem();
OnNewContainer(checkBoxTreeViewItem);
return checkBoxTreeViewItem;
}
void checkBoxTreeViewItem_Checked(object sender, RoutedEventArgs e)
{
CheckBoxTreeViewItem checkBoxTreeViewItem = sender as CheckBoxTreeViewItem;
Action action = () =>
{
var checkedItem = checkBoxTreeViewItem.Header;
CheckedItems.Add(checkedItem);
};
this.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
}
void checkBoxTreeViewItem_Unchecked(object sender, RoutedEventArgs e)
{
CheckBoxTreeViewItem checkBoxTreeViewItem = sender as CheckBoxTreeViewItem;
Action action = () =>
{
var uncheckedItem = checkBoxTreeViewItem.Header;
CheckedItems.Remove(uncheckedItem);
};
this.Dispatcher.BeginInvoke(action, DispatcherPriority.ContextIdle);
}
public IList CheckedItems
{
get { return (IList)base.GetValue(CheckedItemsProperty); }
set { base.SetValue(CheckedItemsProperty, value); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
CheckBoxTreeViewItem.cs
namespace CheckBoxTreeViewLibrary
{
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(CheckBoxTreeViewItem))]
public class CheckBoxTreeViewItem : TreeViewItem
{
public static readonly RoutedEvent CheckedEvent = EventManager.RegisterRoutedEvent("Checked",
RoutingStrategy.Direct,
typeof(RoutedEventHandler),
typeof(CheckBoxTreeViewItem));
public static readonly RoutedEvent UncheckedEvent = EventManager.RegisterRoutedEvent("Unchecked",
RoutingStrategy.Direct,
typeof(RoutedEventHandler),
typeof(CheckBoxTreeViewItem));
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.Register("IsChecked",
typeof(bool),
typeof(CheckBoxTreeViewItem),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
CheckedPropertyChanged));
private static void CheckedPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
CheckBoxTreeViewItem checkBoxTreeViewItem = (CheckBoxTreeViewItem)source;
if (checkBoxTreeViewItem.IsChecked == true)
{
checkBoxTreeViewItem.OnChecked(new RoutedEventArgs(CheckedEvent, checkBoxTreeViewItem));
}
else
{
checkBoxTreeViewItem.OnUnchecked(new RoutedEventArgs(UncheckedEvent, checkBoxTreeViewItem));
}
}
public CheckBoxTreeViewItem()
: base()
{
}
protected override DependencyObject GetContainerForItemOverride()
{
PropertyInfo parentTreeViewPi = typeof(TreeViewItem).GetProperty("ParentTreeView", BindingFlags.Instance | BindingFlags.NonPublic);
CheckBoxTreeView parentCheckBoxTreeView = parentTreeViewPi.GetValue(this, null) as CheckBoxTreeView;
CheckBoxTreeViewItem checkBoxTreeViewItem = new CheckBoxTreeViewItem();
parentCheckBoxTreeView.OnNewContainer(checkBoxTreeViewItem);
return checkBoxTreeViewItem;
}
[Category("Behavior")]
public event RoutedEventHandler Checked
{
add
{
AddHandler(CheckedEvent, value);
}
remove
{
RemoveHandler(CheckedEvent, value);
}
}
[Category("Behavior")]
public event RoutedEventHandler Unchecked
{
add
{
AddHandler(UncheckedEvent, value);
}
remove
{
RemoveHandler(UncheckedEvent, value);
}
}
public bool IsChecked
{
get { return (bool)base.GetValue(IsCheckedProperty); }
set { base.SetValue(IsCheckedProperty, value); }
}
protected virtual void OnChecked(RoutedEventArgs e)
{
base.RaiseEvent(e);
}
protected virtual void OnUnchecked(RoutedEventArgs e)
{
base.RaiseEvent(e);
}
}
}
CheckBoxTreeViewItemStyle.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cti="clr-namespace:CheckBoxTreeViewLibrary">
<Style x:Key="TreeViewItemFocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<PathGeometry x:Key="TreeArrow" Figures="M0,0 L0,6 L6,0 z"/>
<Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="Focusable" Value="False"/>
<Setter Property="Width" Value="16"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="Transparent" Height="16" Padding="5,5,5,5" Width="16">
<Path x:Name="ExpandPath" Data="{StaticResource TreeArrow}" Fill="Transparent" Stroke="#FF989898">
<Path.RenderTransform>
<RotateTransform Angle="135" CenterY="3" CenterX="3"/>
</Path.RenderTransform>
</Path>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="ExpandPath" Value="#FF1BBBFA"/>
<Setter Property="Fill" TargetName="ExpandPath" Value="Transparent"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="RenderTransform" TargetName="ExpandPath">
<Setter.Value>
<RotateTransform Angle="180" CenterY="3" CenterX="3"/>
</Setter.Value>
</Setter>
<Setter Property="Fill" TargetName="ExpandPath" Value="#FF595959"/>
<Setter Property="Stroke" TargetName="ExpandPath" Value="#FF262626"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type cti:CheckBoxTreeViewItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
<Setter Property="Padding" Value="1,0,0,0"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="FocusVisualStyle" Value="{StaticResource TreeViewItemFocusVisual}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type cti:CheckBoxTreeViewItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="15" Width="Auto"/>
<!--<ColumnDefinition Width="Auto"/>-->
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="15"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ToggleButton x:Name="Expander" ClickMode="Press" IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" Style="{StaticResource ExpandCollapseToggleStyle}"/>
<Border x:Name="Bd" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Grid.Column="1" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
<StackPanel Orientation="Horizontal">
<CheckBox Margin="0,2,4,0" x:Name="PART_CheckedCheckBox" IsChecked="{Binding IsChecked, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
<ContentPresenter x:Name="PART_Header" ContentSource="Header" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</StackPanel>
</Border>
<ItemsPresenter x:Name="ItemsHost" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="1"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter Property="Visibility" TargetName="ItemsHost" Value="Collapsed"/>
</Trigger>
<Trigger Property="HasItems" Value="false">
<Setter Property="Visibility" TargetName="Expander" Value="Hidden"/>
</Trigger>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="true"/>
<Condition Property="IsSelectionActive" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="VirtualizingStackPanel.IsVirtualizing" Value="true">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>