Custom DependencyProperty in style triggers using TemplateBinding - c#

I'm having an issue getting the following to work. I have created a subclass of button with several DependencyProperties. and we are attempting to use these in the style.
We have the following c#
/// <summary>
/// Custom Is Mouse Over Colour.
/// </summary>
public static DependencyProperty LITIsMouseOverProperty =
DependencyProperty.RegisterAttached("LITIsMouseOver",
typeof(System.Windows.Media.LinearGradientBrush), typeof(Button));
public static System.Windows.Media.LinearGradientBrush GetLITIsMouseOver(DependencyObject target)
{
return (System.Windows.Media.LinearGradientBrush)target.GetValue(LITIsMouseOverProperty);
}
public static void SetLITIsMouseOver(DependencyObject target, System.Windows.Media.LinearGradientBrush value)
{
target.SetValue(LITIsMouseOverProperty, value);
}
and the following XAML:
<Style x:Key="StandardButton" TargetType="{x:Type Utils:LITCustomButton01}">
<Setter Property="LITIsMouseOver" Value="{StaticResource DarkBrush}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Utils:LITCustomButton01}">
<Border x:Name="buttonBorder" CornerRadius="{TemplateBinding LITCornerRadius}" BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}">
<TextBlock Name="textBlock" Text="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{TemplateBinding Foreground}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" Value="{TemplateBinding LITIsMouseOver}" />
</Trigger>
</Style.Triggers>
</Style>
We have this same system working on corner radius, using the following:
/// <summary>
/// Custom Corner Radius
/// </summary>
public static DependencyProperty LITCornerRadiusProperty =
DependencyProperty.RegisterAttached("LITCornerRadius",
typeof(CornerRadius), typeof(Button));
public static CornerRadius GetLITCornerRadius(DependencyObject target)
{
return (CornerRadius)target.GetValue(LITCornerRadiusProperty);
}
public static void SetLITCornerRadius(DependencyObject target, CornerRadius value)
{
target.SetValue(LITCornerRadiusProperty, value);
}
However, the background throws the following error:
"member is not valid because it does not have a qualifying type name."

TemplateBinding can only be used in a ControlTemplate. The TemplateBinding shortcut does not work in triggers so you will have to use the RelativeSource binding. When overriding the ControlTemplate you will need to specify the target of the trigger. Something like this:
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="Background"
TargetName="buttonBorder"
Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=LITIsMouseOver}"/>
</Trigger>
</ControlTemplate.Triggers>
Also, you are creating an attached property which would allow you to do:
<Grid Utils:LITCustomButton01.LITIsMouseOver="Pink"/>
In this case, nothing will happen on MouseOver. You want to use Register as opposed to RegisterAttached.

Related

Why does a custom control "ImageButton" not display it's image?

I'm writing an image button custom control with highlighting effects, based on MahApps' AccentedSquareButtonStyle. ImageButton.xaml:
<UserControl x:Class="NQR_GUI_WPF.ImageButton"
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:NQR_GUI_WPF"
mc:Ignorable="d" >
<Button Style="{StaticResource AccentedSquareButtonStyle}" Background="Transparent" Foreground="Transparent" BorderThickness="0" Width="24" Height="24" TouchDown="Button_TouchDown">
<Grid Background="Transparent">
<ContentControl>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="Content" Value="{Binding Image, RelativeSource={RelativeSource TemplatedParent}}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=IsMouseOver}" Value="True" >
<Setter Property="Content" Value="{Binding HighlightedImage, RelativeSource={RelativeSource TemplatedParent}}"/>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=IsPressed}" Value="True" >
<Setter Property="Content" Value="{Binding ClickedImage, RelativeSource={RelativeSource TemplatedParent}}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</Grid>
</Button>
ImageButton.xaml.cs:
namespace NQR_GUI_WPF
{
/// <summary>
/// Interaction logic for ImageButton.xaml
/// </summary>
public partial class ImageButton : UserControl
{
public static DependencyProperty ImageProperty = DependencyProperty.Register("Image", typeof(Canvas), typeof(ImageButton));
public static DependencyProperty ClickedImageProperty = DependencyProperty.Register("ClickedImage", typeof(Canvas), typeof(ImageButton));
public static DependencyProperty HighlightedImageProperty = DependencyProperty.Register("HighlightedImage", typeof(Canvas), typeof(ImageButton));
static ImageButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ImageButton), new FrameworkPropertyMetadata(typeof(ImageButton)));
}
public Canvas Image
{
get { return (Canvas)base.GetValue(ImageProperty); }
set { base.SetValue(ImageProperty, value); }
}
public Canvas ClickedImage
{
get { return (Canvas)base.GetValue(ClickedImageProperty); }
set { base.SetValue(ClickedImageProperty, value); }
}
public Canvas HighlightedImage
{
get { return (Canvas)base.GetValue(HighlightedImageProperty); }
set { base.SetValue(HighlightedImageProperty, value); }
}
private void Button_TouchDown(object sender, TouchEventArgs e)
{
Keyboard.ClearFocus();
}
}
}
Example icon:
<Canvas x:Key="printIcon" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="appbar_printer_text" Width="76" Height="76" Clip="F1 M 0,0L 76,0L 76,76L 0,76L 0,0">
<Path Width="44" Height="45" Canvas.Left="16" Canvas.Top="17" Stretch="Fill" Fill="{Binding Source={x:Static prop:Settings.Default}, Path=theme, Converter={StaticResource idealForegroundConverter}}" Data="F1 M 25,27L 25,17L 51,17L 51,27L 47,27L 47,21L 29,21L 29,27L 25,27 Z M 16,28L 60,28L 60,51L 52,51L 52,46L 55,46L 55,33L 21,33L 21,46L 24,46L 24,51L 16,51L 16,28 Z M 25,39L 28,39L 28,52L 35,52L 35,59L 48,59L 48,39L 51,39L 51,62L 33,62L 25,54L 25,39 Z M 46,55L 38,55L 38,52L 46,52L 46,55 Z M 46,49L 30,49L 30,46L 46,46L 46,49 Z M 46,43L 30,43L 30,40L 46,40L 46,43 Z "/>
</Canvas>
The problem is that in MainWindow, after adding the images stored in App.xaml, the control is empty (no images are shown).
<local:ImageButton Image="{StaticResource printIcon}" HighlightedImage="{StaticResource printIconHighlighted}" ClickedImage="{StaticResource printIconClicked}" Grid.Column="1" HorizontalAlignment="Left" Height="46" Margin="36,10,0,0" VerticalAlignment="Top" Width="100"/>
I have tried binding the images directly into the control template, but without success (although in the control designer view the image is shown). Why aren't the control images displayed?
A UserControl isn't your best option for this. UserControls aren't meant for writing general-purpose WPF controls. You can do it, but it's not the simplest way. The simplest way is to subclass a regular control (often just ContentControl or HeaderedContentControl), then write a style and a template for it. Once you get this technique nailed down you can just bang 'em out as needed. Often you can just write a specialized template for an existing control, but in your case you do need your own subclass of Button.
I would write ImageButton as a subclass of Button, with the additional dependency properties pretty much as you've defined them, but I'd make them of type Object so a consumer can stuff anything in there that XAML can render. No reason not to give them all the rope they can use. And I'll use the Content property instead of the Image property, because that simplifies things.
If for some reason you have a requirement to prevent non-image content, you could use a more specialized content type than Object, but you didn't mention any particular reason for introducing that limitation.
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace NQR_GUI_WPF
{
/// <summary>
/// Interaction logic for ImageButton.xaml
/// </summary>
public class ImageButton : Button
{
public ImageButton()
{
TouchDown += ImageButton_TouchDown;
}
private void ImageButton_TouchDown(object sender, TouchEventArgs e)
{
Keyboard.ClearFocus();
}
#region Dependency Properties
public static DependencyProperty ClickedContentProperty = DependencyProperty.Register("ClickedContent", typeof(Object), typeof(ImageButton));
public static DependencyProperty HighlightedContentProperty = DependencyProperty.Register("HighlightedContent", typeof(Object), typeof(ImageButton));
public Object ClickedContent
{
get { return (Object)base.GetValue(ClickedContentProperty); }
set { base.SetValue(ClickedContentProperty, value); }
}
public Object HighlightedContent
{
get { return (Object)base.GetValue(HighlightedContentProperty); }
set { base.SetValue(HighlightedContentProperty, value); }
}
#endregion Dependency Properties
}
}
XAML resoure dictionary ImageButton.xaml:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:nqrgui="clr-namespace:NQR_GUI_WPF"
>
<Style TargetType="{x:Type nqrgui:ImageButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type nqrgui:ImageButton}">
<Grid>
<ContentControl
Content="{TemplateBinding Content}"
x:Name="PART_Content"
/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter
TargetName="PART_Content"
Property="Content"
Value="{Binding HighlightedContent, RelativeSource={RelativeSource TemplatedParent}}"
/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter
TargetName="PART_Content"
Property="Content"
Value="{Binding ClickedContent, RelativeSource={RelativeSource TemplatedParent}}"
/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
And here's how you'd use it:
<Window
...
xmlns:nqrgui="clr-namespace:NQR_GUI_WPF"
...
>
<!-- Or better yet, merge ImageButton.xaml in App.xaml so everybody can see it -->
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ImageButton.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
...
<!-- As noted, Content, HighlightedContent, and ClickedContent
can be images -- or also paths, text, ANYTHING XAML can render.
-->
<nqrgui:ImageButton
Content="Content"
HighlightedContent="Highlighted"
ClickedContent="Clicked"
/>
And you really can go absolutely berserk with the content:
<!-- Don't try this in a UI anybody will have to use! -->
<nqrgui:ImageButton
Content="Content"
ClickedContent="Clicked"
>
<nqrgui:ImageButton.HighlightedContent>
<StackPanel Orientation="Horizontal">
<Border
BorderBrush="Gray"
Background="GhostWhite"
BorderThickness="1">
<Path
Width="20"
Height="20"
Stroke="Black"
StrokeThickness="2"
Data="M 0,0 L 20,20 M 0,20 L 20,0"
Margin="2"
/>
</Border>
<nqrgui:ImageButton
Content="LOL"
ClickedContent="Don't Click Me, Bro!"
HighlightedContent="I heard you like buttons"
/>
</StackPanel>
</nqrgui:ImageButton.HighlightedContent>
</nqrgui:ImageButton>
You are using TemplateParent incorrectly
instead of this
{Binding Image, RelativeSource={RelativeSource TemplatedParent}}
it should be something like this
{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=ImageButton}, Path=Image}
I have done that like this below,
<Controls:MetroWindow.Resources>
<ImageBrush Stretch="Fill" x:Key="CloseImage" ImageSource="../images/Close.png" />
<ImageBrush x:Key="CloseImageRed" ImageSource="../images/CloseRed.jpg" />
</Controls:MetroWindow.Resources>
<Button>
<Button.Style>
<Style TargetType="Button">
<Setter Property="Background" Value="{StaticResource CloseImageRed}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
RecognizesAccessKey="True"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource CloseImage}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
Have a look.
You are setting UserControl.Content to your customized Button, and I think what you want to be setting is UserControl.ContentTemplate.
From within .Content, there is no "TemplatedParent" to bind to. However if this were a Template, then TemplatedParent would point to the UserControl that the Template is defined for. In this case, it would refer to your ImageButton UserControl, which would correctly give you access to the Image properties.
<UserControl ..>
<UserControl.ContentTemplate>
<ControlTemplate>
<!-- TemplatedParent bindings should refer to UserControl from here -->
<Button ... />
</ControlTemplate>
</UserControl.ContentTemplate>
</UserControl>
This also allows you to write something like
<local:ImageButton Content="Some Text" />
without completely replacing your Button XAML code with a Text element containing "Some Text"
For an example, what you have right now would render as
<UserControl>
<Button /> <!-- Button is .Content, and can be replaced by XAML using the control -->
</UserControl>
While if it were a ContentTemplate, it would render as
<UserControl>
<Button> <!-- Button is ContentTemplate, so wraps any content given by external XAML -->
<Content />
</Button>
</UserControl>

WPF Custom Control Button Content Goes Missing With More Than One Button

To begin with, this is in .NET 4.0 because it has to be. I know some bugs have been fixed in later versions of .NET, so if this is an actual .NET bug, I guess I'm going to have to live with using user controls which don't seem to have this issue.
I created a custom control library in WPF to make customizable buttons that will be used in 3rd party software. I seem to have an issue, however, with multiple buttons resulting in the content for all but one of the buttons to go missing. I have confirmed the problem in SNOOP. The content just isn't there. The SNOOP tree gets as far as the content presenter and then there's nothing under it, except for the one button that does have content. I've created a very bare bones example of the problem.
My Library's Generic.xaml is as follows:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:CustomControlsLibrary.Controls">
<Style x:Key="CustomButtonStyle" TargetType="{x:Type controls:CustomButton}">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:CustomButton}">
<Border CornerRadius="{TemplateBinding CornerRadius}" BorderThickness="3" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" ContentSource="Content" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="Button1Style" TargetType="{x:Type controls:Button1}" BasedOn="{StaticResource CustomButtonStyle}" >
<Setter Property="CornerRadius" Value="4" />
<Setter Property="BorderBrush" Value="White" />
<Setter Property="Height" Value="40" />
<Setter Property="Width" Value="100" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Content">
<Setter.Value>
<TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=controls:Button1}, Path=Text}" />
</Setter.Value>
</Setter>
</Style>
The two control classes are as follows:
CustomButton:
public class CustomButton : Button
{
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(CustomButton), new FrameworkPropertyMetadata(new CornerRadius(0)));
public CornerRadius CornerRadius
{
get { return (CornerRadius)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
static CustomButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomButton), new FrameworkPropertyMetadata(typeof(CustomButton)));
}
}
Button1:
public class Button1 : CustomButton
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(Button1), new FrameworkPropertyMetadata(""));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
static Button1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Button1), new FrameworkPropertyMetadata(typeof(Button1)));
}
}
I then create a simple WPF application with just a main window with all logic in MainWindow.xaml:
<Window x:Class="CustomControlLibraryTestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:CustomControlsLibrary.Controls;assembly=CustomControlsLibrary"
Title="MainWindow" Height="350" Width="525" Background="DarkGray">
<Window.Resources>
<ResourceDictionary Source="pack://application:,,,/CustomControlsLibrary;component/Themes/Generic.xaml" />
</Window.Resources>
<StackPanel>
<controls:Button1 Style="{StaticResource Button1Style}" Background="Red" Text="Button 1" />
<controls:Button1 Style="{StaticResource Button1Style}" Background="Blue" Text="Button 2" />
</StackPanel>
When run, the content for Button 1 goes missing while Button 2 looks just fine. Removing Button 2 from the Window causes Button 1 to look as expected.
And as mentioned earlier, SNOOP indicates that Button 1's content is just not there when both buttons are present.
Any ideas?
I'm going to throw in a dissenting opinion here, starting with a quote from Matthew MacDonalds "Pro WPF in C#":
Custom controls are still a useful way to build custom widgets that
you can share between applications, but they’re no longer a
requirement when you want to enhance and customize core controls. (To
understand how remarkable this change is, it helps to point out that
this book’s predecessor, Pro .NET 2.0 Windows Forms and Custom
Controls in C#, had nine complete chapters about custom controls and
additional examples in other chapters. But in this book, you’ve made
it to Chapter 18 without a single custom control sighting!)
Put simply, there is just no need to be creating extra button classes just to control properties that already exist in the templates. You can do that just as easily with data binding or attached properties etc and it will be a lot more compatible with tools like Blend.
To illustrate the point here's a helper class for the two properties you're exposing in your sample code:
public static class ButtonHelper
{
public static double GetCornerRadius(DependencyObject obj)
{
return (double)obj.GetValue(CornerRadiusProperty);
}
public static void SetCornerRadius(DependencyObject obj, double value)
{
obj.SetValue(CornerRadiusProperty, value);
}
// Using a DependencyProperty as the backing store for CornerRadius. This enables animation, styling, binding, etc...
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.RegisterAttached("CornerRadius", typeof(double), typeof(ButtonHelper), new PropertyMetadata(0.0));
public static string GetButtonText(DependencyObject obj)
{
return (string)obj.GetValue(ButtonTextProperty);
}
public static void SetButtonText(DependencyObject obj, string value)
{
obj.SetValue(ButtonTextProperty, value);
}
// Using a DependencyProperty as the backing store for ButtonText. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ButtonTextProperty =
DependencyProperty.RegisterAttached("ButtonText", typeof(string), typeof(ButtonHelper), new PropertyMetadata(""));
}
Now we can immediately create two style, one for each of your button types, that bind to these properties internally:
<Style x:Key="RoundedButtonStyle" TargetType="{x:Type Button}" >
<Setter Property="Margin" Value="10" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="Background" Value="Red" />
<Setter Property="controls:ButtonHelper.CornerRadius" Value="4" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="{Binding Path=(controls:ButtonHelper.CornerRadius),
RelativeSource={RelativeSource TemplatedParent}}" BorderThickness="3"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" ContentSource="Content" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TextButtonStyle" TargetType="{x:Type Button}" BasedOn="{StaticResource RoundedButtonStyle}">
<Setter Property="BorderBrush" Value="Blue" />
<Setter Property="Background" Value="Blue" />
<Setter Property="controls:ButtonHelper.ButtonText" Value="TextButton" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border CornerRadius="{Binding Path=(controls:ButtonHelper.CornerRadius),
RelativeSource={RelativeSource TemplatedParent}}" BorderThickness="3"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}">
<TextBlock Text="{Binding Path=(controls:ButtonHelper.ButtonText),
RelativeSource={RelativeSource TemplatedParent}}" Background="Transparent" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
That's it! No custom control needed no need for x:Shared due to content being specified directly in a style, and it's a lot more light-weight. Here's an example of them being used:
<UniformGrid Columns="2">
<Button Style="{StaticResource RoundedButtonStyle}" Content="RoundedButton" />
<Button Style="{StaticResource RoundedButtonStyle}" Content="RoundedButton big radius" controls:ButtonHelper.CornerRadius="20"/>
<Button Style="{StaticResource TextButtonStyle}" />
<Button Style="{StaticResource TextButtonStyle}" controls:ButtonHelper.ButtonText="TextButton new text"/>
<Button Style="{StaticResource TextButtonStyle}" BorderBrush="Green" Background="Green"
controls:ButtonHelper.ButtonText="Both text and radius"
controls:ButtonHelper.CornerRadius="20" />
</UniformGrid>
And here's the result:
I do realize of course that I've specified the border in each template, but that too can be easily removed by placing a content control inside the border and using data templating to set the content.
What's happening is that the style actually has a single TextBlock instance. When the style is applied to the second button the TextBlock is actually re-parented to the new control. You should be able to avoid this by setting x:Shared="false" on the TextBlock element.

Fallback from one DependencyProperty to another if not set

What I want:
I created a UserControl ImageButton, where the images can be set in WPF. Usually, I'm reusing the default image for all states but one, but have to configure the control in a very verbose way (see WPF snipped below). What I'd like to achieve is, that every image that is not set, just uses the default image.
What I have:
This is the way I'm describing the ImageButton at the moment:
<cc:ImageButton x:Name="cmdHideCustomWindowingArea"
DefaultImage="/Images/UI/ButtonDefault.png"
HoverImage="/Images/UI/ButtonDefault.png"
DownImage="/Images/UI/ButtonActive.png"
UpImage="/Images/UI/ButtonDefault.png"
DisabledImage="/Images/UI/ButtonDefault.png"/>
This is the style im using:
<Style TargetType="{x:Type cc:ImageButton}">
<Setter Property="Template" >
<Setter.Value>
<ControlTemplate TargetType="{x:Type cc:ImageButton}">
<Grid x:Name="Grid">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<Image x:Name="ButtonImage" Source="{Binding DefaultImage, RelativeSource={RelativeSource TemplatedParent}}"/>
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" RecognizesAccessKey="True" />
</StackPanel>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="ButtonImage" Property="Source" Value="{Binding HoverImage, RelativeSource={RelativeSource TemplatedParent}}" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="ButtonImage" Property="Source" Value="{Binding DownImage, RelativeSource={RelativeSource TemplatedParent}}" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter TargetName="ButtonImage" Property="Source" Value="{Binding DisabledImage, RelativeSource={RelativeSource TemplatedParent}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And this is the code:
public class ImageButton : Button
{
static ImageButton()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ImageButton),
new FrameworkPropertyMetadata(typeof(ImageButton)));
}
public ImageSource DefaultImage
{
get { return (ImageSource)GetValue(DefaultImageProperty); }
set { SetValue(DefaultImageProperty, value); }
}
public static readonly DependencyProperty DefaultImageProperty =
DependencyProperty.Register("DefaultImage",
typeof(ImageSource), typeof(ImageButton), new PropertyMetadata(null));
public ImageSource HoverImage
{
get { return (ImageSource)GetValue(HoverImageProperty); }
set { SetValue(HoverImageProperty, value); }
}
public static readonly DependencyProperty HoverImageProperty =
DependencyProperty.Register("HoverImage",
typeof(ImageSource), typeof(ImageButton), new PropertyMetadata(null));
//
// ... and so on for every state (Down, Up, Disabled)
//
}
What I've tried so far:
1. I already tried checking if the image is set like this, which does not work and the code seems to never even reach the getter at all...
get { return (ImageSource)GetValue(HoverImageProperty) ?? DefaultImage; }
2. Also, setting a TargetNullValue like recommended here does not work ("The member "TargetNullValue" is not recognized or is not accessible."):
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="ButtonImage" Property="Source"
Value="{Binding DownImage, RelativeSource={RelativeSource TemplatedParent}}"
TargetNullValue="{Binding DefaultImage, RelativeSource={RelativeSource TemplatedParent}}"/>
</Trigger>
3. The last thing I did was trying to set a PriorityBinding, like recommended here:
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="ButtonImage" Property="Source">
<Setter.Value>
<PriorityBinding>
<Binding Path="{Binding HoverImage, RelativeSource={RelativeSource TemplatedParent}}"/>
<Binding Path="{Binding DefaultImage, RelativeSource={RelativeSource TemplatedParent}}"/>
</PriorityBinding>
</Setter.Value>
</Setter>
</Trigger>
But that returns the following error:
A 'Binding' cannot be set on the 'Path' property of type 'Binding'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
What I'll have to do:
What would be the right way to fall back to a another DependencyProperty in case the one I'm looking for is not set?
It sounds as though you are looking for the PriorityBinding Class (despite your earlier attempt at using it). From the linked page, a PriorityBinding:
Describes a collection of Binding objects that is attached to a single binding target property, which receives its value from the first binding in the collection that produces a value successfully.
...
PriorityBinding lets you associate a binding target (target) property with a list of bindings. The first binding that returns a value successfully becomes the active binding.
A binding returns a value successfully if:
1.The path to the binding source resolves successfully.
2.The value converter, if any, is able to convert the resulting value.
3.The resulting value is valid for the target property.
You should rearrange your Binding Path in your PriorityBindings... try this instead:
<PriorityBinding>
<Binding Path="HoverImage" RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="DefaultImage" RelativeSource="{RelativeSource TemplatedParent}" />
</PriorityBinding>
For further information, you can also read through the WPF Tutorial - Priority Bindings tutorial on the Tech Pro website.
UPDATE >>>
As you correctly pointed out, the PriorityBinding will indeed accept null as a valid, returned value. However, you can get around that problem by using a custom IValueConverter implementation that returns DependencyProperty.UnsetValue when the actual value is null.

How to make a WPF resource be recalculated when a trigger's run?

In short: I've got a Style. It uses TemplateBinding a fair bit to make it parametrized instead of repeating myself over and over again. However, when a trigger for that style gets used and a resource gets used in a setter in that trigger, it just doesn't show up! Not even the default value gets shown. Here's a small program that replicates this issue:
TestDictionary.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lcl="clr-namespace:MyNamespace">
<Style TargetType="Button" x:Key="BtnTest">
<Style.Resources>
<Label Content="{TemplateBinding lcl:TestClass.String}" x:Key="innerLabel"/>
</Style.Resources>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Content" Value="{DynamicResource innerLabel}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
MainWindow.xaml
<Window x:Class="MyNamespace.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:lcl="clr-namespace:MyNamespace"
Title="Test" Width="500" Height="350">
<Window.Resources>
<ResourceDictionary Source="TestDictionary.xaml"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button Content="Enable/Disable" Click="Click"/>
<Button Grid.Column="1" x:Name="btn" Style="{DynamicResource BtnTest}" lcl:TestClass.String="TESTING"/>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace MyNamespace
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Click(object sender, RoutedEventArgs e)
{
btn.IsEnabled = !btn.IsEnabled;
}
}
public class TestClass
{
public static string GetString(DependencyObject obj)
{
return (string)obj.GetValue(StringProperty);
}
public static void SetString(DependencyObject obj, string value)
{
obj.SetValue(StringProperty, value);
}
public static readonly DependencyProperty StringProperty =
DependencyProperty.RegisterAttached("String", typeof(string), typeof(TestClass), new PropertyMetadata("Default!"));
}
}
Instead of using a TemplateBinding, I also tried this:
{Binding Path=lcl:TestClass.String, RelativeSource={RelativeSource AncestorType={x:Type Button}}}
It still didn't work.
I know I'm probably doing something wrong, but the question is: what is it?
All you really need to make this work is to use RelativeSource in your binding. Since you are setting the attached property on the Button, in your style trigger, you can just bind to the attached property on self:
<Style TargetType="Button" x:Key="BtnTest">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Content"
Value="{Binding Path=(lcl:TestClass.String), RelativeSource={RelativeSource Self}}"/>
</Trigger>
</Style.Triggers>
</Style>
One cool thing about using your approach, since Button is a ContentControl, you're attached property can be any object, not just strings.
And to clarify what went wrong in your previous approach -
As others have said, TemplateBinding only works in ControlTemplates. It also only works when the DependencyProperty is defined on the class you are creating the template for (so you can never do a TemplateBinding to Grid.Row for example)
When binding to an attached property, the whole thing needs to be in parentheses, otherwise WPF will try to bind to a property of a property. Otherwise your RelativeSource binding was close!
I think if you want to have a Label inside the Button as the content, it may work (I didn't test that), but it doesn't seem like the best idea, as your Button can host any object you want.
EDIT for more complex example
So, if you need to display more than one dynamic property, I would recommend using a DataTemplate:
<Style TargetType="Button" x:Key="BtnTest">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Label Content="{Binding Path=(lcl:TestClass.String), RelativeSource={RelativeSource AncestorType={x:Type Button}}}" />
</DataTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
Also, I want to point out that a DataTemplateSelector might be more applicable if you have multiple different criteria for changing the look of the content.
Now I see the details. What you should write before relative source is like:
Binding Path=(lcl:TestClass.String)
Do not forget to add parenthesis.
Your example does not work because TemplateBinding only works in a ControlTemplate. To achieve something akin to a TemplateBinding in Resources you need to do other stuff. Here's an example.
In order for TemplateBinding to work, you need to fix the code a little bit (this is just an example with no resources):
<Style x:Key="BtnTest" TargetType="{x:Type Button}">
<Setter Property="MinHeight" Value="100" />
<Setter Property="MinWidth" Value="200" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="BorderBrush" Value="Blue" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2" Background="{TemplateBinding Background}">
<ContentPresenter RecognizesAccessKey="True" Content="{TemplateBinding lcl:TestClass.String}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
</Style.Triggers>
</Style>
Useful links about this topic: Here, and here too.
EDIT:
You can also use the application settings instead of TestClass. Open "Project -> Properties: MyNamespace... -> Settings" and add your settings:
Name--------Type--------Scope--------Value
LabelText---string--------User----------Default
Set the your value for the LabelText in code. For example:
public MainWindow()
{
InitializeComponent();
MyNamespace.Properties.Settings.Default.LabelText = "Testing";
}
And use this ResourceDictionary:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:properties="clr-namespace:MyNamespace.Properties"
xmlns:lcl="clr-namespace:MyNamespace">
<Style TargetType="Button" x:Key="BtnTest">
<Style.Resources>
<Label x:Key="innerLabel" Content="{Binding Source={x:Static properties:Settings.Default}, Path=LabelText, Mode=TwoWay}" />
</Style.Resources>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="True">
<Setter Property="Content" Value="{DynamicResource innerLabel}"/>
</Trigger>
</Style.Triggers>
</Style>

WPF ControlTemplate for scrolling TreeView Control

I'm using a the TreeView control and it scrolls automatically to left-align TreeViewItem when one of them is clicked. I've gone looking at my Styles and ControlTemplates, but I haven't found anything. Is there a default ControlTemplate that causes this? I want to disable it.
The items scroll because the ScrollViewer calls BringIntoView() on them. So one way to avoid scrolling is to suppress the handling of the RequestBringIntoView event. You can try that out quickly by subclassing TreeView and instantiating this control instead:
public class NoScrollTreeView : TreeView
{
public class NoScrollTreeViewItem : TreeViewItem
{
public NoScrollTreeViewItem() : base()
{
this.RequestBringIntoView += delegate (object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true;
};
}
protected override DependencyObject GetContainerForItemOverride()
{
return new NoScrollTreeViewItem();
}
}
protected override DependencyObject GetContainerForItemOverride()
{
return new NoScrollTreeViewItem();
}
}
after spending some hours on this problem i found a solution that works for me.
brians solution to prevent the RequestBringIntoView event on a TreeViewItem from bubbling was the first step. unfortunately this also stops a treeviewitem to be shown if you change the selected item programmatically by
yourtreeview.SelectedItem = yourtreeviewitem
so, for me the solution is to modify the controltemplate of the treeview as follows:
<Style x:Key="{x:Type TreeView}" TargetType="TreeView">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="SnapsToDevicePixels" Value="True" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeView">
<Border Name="Border" BorderThickness="0" Padding="0" Margin="1">
<ScrollViewer Focusable="False" CanContentScroll="False" Padding="0">
<Components:AutoScrollPreventer Margin="0">
<ItemsPresenter/>
</Components:AutoScrollPreventer>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
the "autoscrollpreventer" is:
using System;
using System.Windows;
using System.Windows.Controls;
namespace LiveContext.Designer.GUI.Components {
public class AutoScrollPreventer : StackPanel
{
public AutoScrollPreventer() {
this.RequestBringIntoView += delegate(object sender, RequestBringIntoViewEventArgs e)
{
// stop this event from bubbling so that a scrollviewer doesn't try to BringIntoView..
e.Handled = true;
};
}
}
}
hope it helps..
It looks like I found a good clue on MSDN:
Sounds like this is an interaction
with the scrollviewer and the focus
system.
When an element is focused within a
ScrollViewer (which is part of the
TreeView template), the ScrollViewer
is instructed to make the element
visible. It automatically responds by
scrolling to the requested element.
The methods inside of ScrollViewer
that handle these focus requests are
all private and / or internal so you
really can't get to them. I don't
think there's too much you can do in
this case; it's just how focus works.
So, is that it? Surely there's a way to modify the TreeView template so that the ScrollViewer won't have this behavior...
Ok, I was finally able to get the default style like this:
using (Stream sw = File.Open(#"C:\TreeViewDefaults.xaml", FileMode.Truncate, FileAccess.Write))
{
Style ts = Application.Current.FindResource(typeof(TreeView)) as Style;
if (ts != null)
XamlWriter.Save(ts, sw);
}
Which produced:
<Style TargetType="TreeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style.Triggers>
<Trigger Property="VirtualizingStackPanel.IsVirtualizing">
<Setter Property="ItemsControl.ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate><VirtualizingStackPanel IsItemsHost="True" /></ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
</Style.Triggers>
<Style.Resources>
<ResourceDictionary />
</Style.Resources>
<Setter Property="Panel.Background">
<Setter.Value><DynamicResource ResourceKey="{x:Static SystemColors.WindowBrushKey}" /></Setter.Value>
</Setter>
<Setter Property="Border.BorderBrush">
<Setter.Value><SolidColorBrush>#FF828790</SolidColorBrush></Setter.Value>
</Setter>
<Setter Property="Border.BorderThickness">
<Setter.Value><Thickness>1,1,1,1</Thickness></Setter.Value>
</Setter>
<Setter Property="Control.Padding">
<Setter.Value><Thickness>1,1,1,1</Thickness></Setter.Value>
</Setter>
<Setter Property="TextElement.Foreground">
<Setter.Value><DynamicResource ResourceKey="{x:Static SystemColors.ControlTextBrushKey}" /></Setter.Value>
</Setter>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility">
<Setter.Value><x:Static Member="ScrollBarVisibility.Auto" /></Setter.Value>
</Setter>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility">
<Setter.Value><x:Static Member="ScrollBarVisibility.Auto" /></Setter.Value>
</Setter>
<Setter Property="Control.VerticalContentAlignment">
<Setter.Value><x:Static Member="VerticalAlignment.Center" /></Setter.Value>
</Setter>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="TreeView">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Name="Bd" SnapsToDevicePixels="True">
<ScrollViewer CanContentScroll="False"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
Background="{TemplateBinding Panel.Background}"
Padding="{TemplateBinding Control.Padding}"
Name="_tv_scrollviewer_"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}"
Focusable="False">
<ItemsPresenter />
</ScrollViewer>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="UIElement.IsEnabled">
<Setter Property="Panel.Background" TargetName="Bd">
<Setter.Value>
<DynamicResource ResourceKey="{x:Static SystemColors.ControlBrushKey}" />
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>False</s:Boolean>
</Trigger.Value>
</Trigger>
<Trigger Property="VirtualizingStackPanel.IsVirtualizing">
<Setter Property="ScrollViewer.CanContentScroll" TargetName="_tv_scrollviewer_">
<Setter.Value><s:Boolean>True</s:Boolean></Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Which, unfortunately, doesn't look helpful. I don't see any properties in there for stopping the auto-scroll-focus thing.
Still looking...
Another fun tidbit: there is a overridable boolean value called HandlesScrolling that always returns true. After decompiling the source, it looks like this property is NEVER used (or it's being used in some deep, dark, secret place in XAML). I tried making my own TreeView control to set this value to false and it didn't work.

Categories

Resources