I'm trying to create a prompt text label in the background of a TextBox using attached properties, but I can't resolve the binding to the text caption in a style resource:
Style definition:
<Style x:Key="CueBannerTextBoxStyle"
TargetType="TextBox">
<Style.Resources>
<VisualBrush x:Key="CueBannerBrush"
AlignmentX="Left"
AlignmentY="Center"
Stretch="None">
<VisualBrush.Visual>
<Label Content="{Binding Path=(EnhancedControls:CueBannerTextBox.Caption), RelativeSource={RelativeSource AncestorType={x:Type TextBox}}}"
Foreground="LightGray"
Background="White"
Width="200" />
</VisualBrush.Visual>
</VisualBrush>
</Style.Resources>
<Style.Triggers>
<Trigger Property="Text"
Value="{x:Static sys:String.Empty}">
<Setter Property="Background"
Value="{DynamicResource CueBannerBrush}" />
</Trigger>
<Trigger Property="Text"
Value="{x:Null}">
<Setter Property="Background"
Value="{DynamicResource CueBannerBrush}" />
</Trigger>
<Trigger Property="IsKeyboardFocused"
Value="True">
<Setter Property="Background"
Value="White" />
</Trigger>
</Style.Triggers>
</Style>
Attached property:
public class CueBannerTextBox
{
public static String GetCaption(DependencyObject obj)
{
return (String)obj.GetValue(CaptionProperty);
}
public static void SetCaption(DependencyObject obj, String value)
{
obj.SetValue(CaptionProperty, value);
}
public static readonly DependencyProperty CaptionProperty =
DependencyProperty.RegisterAttached("Caption", typeof(String), typeof(CueBannerTextBox), new UIPropertyMetadata(null));
}
Usage:
<TextBox x:Name="txtProductInterfaceStorageId"
EnhancedControls:CueBannerTextBox.Caption="myCustomCaption"
Width="200"
Margin="5"
Style="{StaticResource CueBannerTextBoxStyle}" />
The idea is that you can define the text prompt used in the visual brush when you create the textbox, but I'm getting a binding error:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.TextBox', AncestorLevel='1''. BindingExpression:Path=(0); DataItem=null; target element is 'Label' (Name=''); target property is 'Content' (type 'Object')
The code works fine if I just hardcode the Label.Content property in the style.
Any ideas?
The problem here has to do with the way Style works: basically, one "copy" of the Style will be created (at first reference), and at that point, there may be multiple TextBox controls you want this Style applied to - which one will it use for the RelativeSource?
The (probable) answer is to use a Template instead of a Style - with a control or data template, you'll be able to access the visual tree of the TemplatedParent, and that should get you where you need to be.
EDIT: On further thought, I may be incorrect here...I'll throw together a quick test harness when I'm back in front of a computer and see if I can prove/disprove this.
FURTHER EDIT: While what I originally said was arguably "true", that's not your problem; What Raul said re: the visual tree is correct:
You are setting the Background property on the TextBox to a VisualBrush instance.
The Visual of that brush is not mapped into the Visual Tree of the control.
As a result, any {RelativeSource FindAncestor} navigation will fail, as the parent of that visual will be null.
This is the case regardless of whether it is declared as a Style or a ControlTemplate.
All that said, relying on ElementName definitely is non-ideal, as it reduces the reusability of the definition.
So, what to do?
I've been wracking my brain overnight trying to think of a way to marshal over the proper inheritance context to the contained brush, with little success...I did come up with this super-hacky way, however:
First, the helper property (note: I don't usually style my code this way, but trying to save space):
public class HackyMess
{
public static String GetCaption(DependencyObject obj)
{
return (String)obj.GetValue(CaptionProperty);
}
public static void SetCaption(DependencyObject obj, String value)
{
Debug.WriteLine("obj '{0}' setting caption to '{1}'", obj, value);
obj.SetValue(CaptionProperty, value);
}
public static readonly DependencyProperty CaptionProperty =
DependencyProperty.RegisterAttached("Caption", typeof(String), typeof(HackyMess),
new FrameworkPropertyMetadata(null));
public static object GetContext(DependencyObject obj) { return obj.GetValue(ContextProperty); }
public static void SetContext(DependencyObject obj, object value) { obj.SetValue(ContextProperty, value); }
public static void SetBackground(DependencyObject obj, Brush value) { obj.SetValue(BackgroundProperty, value); }
public static Brush GetBackground(DependencyObject obj) { return (Brush) obj.GetValue(BackgroundProperty); }
public static readonly DependencyProperty ContextProperty = DependencyProperty.RegisterAttached(
"Context", typeof(object), typeof(HackyMess),
new FrameworkPropertyMetadata(default(HackyMess), FrameworkPropertyMetadataOptions.OverridesInheritanceBehavior | FrameworkPropertyMetadataOptions.Inherits));
public static readonly DependencyProperty BackgroundProperty = DependencyProperty.RegisterAttached(
"Background", typeof(Brush), typeof(HackyMess),
new UIPropertyMetadata(default(Brush), OnBackgroundChanged));
private static void OnBackgroundChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var rawValue = args.NewValue;
if (rawValue is Brush)
{
var brush = rawValue as Brush;
var previousContext = obj.GetValue(ContextProperty);
if (previousContext != null && previousContext != DependencyProperty.UnsetValue)
{
if (brush is VisualBrush)
{
// If our hosted visual is a framework element, set it's data context to our inherited one
var currentVisual = (brush as VisualBrush).GetValue(VisualBrush.VisualProperty);
if(currentVisual is FrameworkElement)
{
(currentVisual as FrameworkElement).SetValue(FrameworkElement.DataContextProperty, previousContext);
}
}
}
// Why can't there be just *one* background property? *sigh*
if (obj is TextBlock) { obj.SetValue(TextBlock.BackgroundProperty, brush); }
else if (obj is Control) { obj.SetValue(Control.BackgroundProperty, brush); }
else if (obj is Panel) { obj.SetValue(Panel.BackgroundProperty, brush); }
else if (obj is Border) { obj.SetValue(Border.BackgroundProperty, brush); }
}
}
}
And now the updated XAML:
<Style x:Key="CueBannerTextBoxStyle"
TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="TextBox.Text"
Value="{x:Static sys:String.Empty}">
<Setter Property="local:HackyMess.Background">
<Setter.Value>
<VisualBrush AlignmentX="Left"
AlignmentY="Center"
Stretch="None">
<VisualBrush.Visual>
<Label Content="{Binding Path=(local:HackyMess.Caption)}"
Foreground="LightGray"
Background="White"
Width="200" />
</VisualBrush.Visual>
</VisualBrush>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsKeyboardFocused"
Value="True">
<Setter Property="local:HackyMess.Background"
Value="White" />
</Trigger>
</Style.Triggers>
</Style>
<TextBox x:Name="txtProductInterfaceStorageId"
local:HackyMess.Caption="myCustomCaption"
local:HackyMess.Context="{Binding RelativeSource={RelativeSource Self}}"
Width="200"
Margin="5"
Style="{StaticResource CueBannerTextBoxStyle}" />
<TextBox x:Name="txtProductInterfaceStorageId2"
local:HackyMess.Caption="myCustomCaption2"
local:HackyMess.Context="{Binding RelativeSource={RelativeSource Self}}"
Width="200"
Margin="5"
Style="{StaticResource CueBannerTextBoxStyle}" />
The problem is that the Label inside the VisualBrush is not a visual child of the TextBox, that is the reason why that binding doesn't work. My solution for that problem will be using the ElementName binding. But the visual brush you are creating is inside a Style's dictionary resource and then the ElementName binding will not work because doesn't find the element id. The solution for that will be to create the VisualBrush in a global dictionary resources. See this XAML code for delcaring the VisualBrush:
<Window.Resources>
<VisualBrush x:Key="CueBannerBrush"
AlignmentX="Left"
AlignmentY="Center"
Stretch="None">
<VisualBrush.Visual>
<Label Content="{Binding Path=(EnhancedControls:CueBannerTextBox.Caption), ElementName=txtProductInterfaceStorageId}"
Foreground="#4F48DD"
Background="#B72121"
Width="200"
Height="200" />
</VisualBrush.Visual>
</VisualBrush>
<Style x:Key="CueBannerTextBoxStyle"
TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Text"
Value="{x:Static System:String.Empty}">
<Setter Property="Background"
Value="{DynamicResource CueBannerBrush}" />
</Trigger>
<Trigger Property="Text"
Value="{x:Null}">
<Setter Property="Background"
Value="{DynamicResource CueBannerBrush}" />
</Trigger>
<Trigger Property="IsKeyboardFocused"
Value="True">
<Setter Property="Background"
Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
This code should works. No more code need to be changed so I'm not rewriting all the code.
Hope this solution works for you...
Related
I have made a custom Control in WPF which is a star rating. When there are 5 stars, these should be golden.
<UserControl x:Class="Lama.Wpf.Controls.RatingControl"
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:controls="clr-namespace:Lama.Wpf.Controls"
mc:Ignorable="d">
<Grid>
<Grid.Resources>
<ControlTemplate x:Key="RatingTemplate" TargetType="{x:Type ToggleButton}">
<Viewbox>
<Path Name="star" Fill="White" Opacity="0.2"
Data="F1 M 145.637,174.227L 127.619,110.39L 180.809,70.7577L 114.528,68.1664L 93.2725,5.33333L 70.3262,67.569L 4,68.3681L 56.0988,109.423L 36.3629,172.75L 91.508,135.888L 145.637,174.227 Z" />
</Viewbox>
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Property="IsChecked" Value="True" />
<Condition
Binding="{Binding Rating, RelativeSource={RelativeSource FindAncestor, AncestorType=controls:RatingControl}}"
Value="5" />
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter TargetName="star" Property="Fill" Value="Gold" />
<Setter TargetName="star" Property="Opacity" Value="1" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0" Tag="1" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
<ToggleButton Grid.Column="1" Tag="2" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
<ToggleButton Grid.Column="2" Tag="3" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
<ToggleButton Grid.Column="3" Tag="4" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
<ToggleButton Grid.Column="4" Tag="5" Padding="2" Template="{StaticResource RatingTemplate}"
Click="ClickEventHandler" />
</Grid>
</UserControl>
public partial class RatingControl
{
private const int Max = 5;
public static readonly DependencyProperty RatingProperty = DependencyProperty.Register(nameof(Rating),
typeof(int),
typeof(RatingControl),
new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
RatingChanged));
public int Rating
{
get => (int)GetValue(RatingProperty);
set
{
if (value < 0)
{
SetValue(RatingProperty, 0);
}
else if (value > Max)
{
SetValue(RatingProperty, Max);
}
else
{
SetValue(RatingProperty, value);
}
}
}
public RatingControl()
{
InitializeComponent();
}
private static void RatingChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var item = sender as RatingControl;
var newVal = (int)e.NewValue;
var children = ((Grid)(item.Content)).Children;
ToggleButton button;
for (var i = 0; i < newVal; i++)
{
button = children[i] as ToggleButton;
if (button != null)
button.IsChecked = true;
}
for (var i = newVal; i < children.Count; i++)
{
button = children[i] as ToggleButton;
if (button != null)
button.IsChecked = false;
}
}
private void ClickEventHandler(object sender, RoutedEventArgs args)
{
var button = sender as ToggleButton;
if (button == null)
{
return;
}
var newValue = int.Parse(button.Tag.ToString());
Rating = newValue;
}
}
If I run this, I get this exception:
InvalidOperationException: Must have non-null value for 'Binding'.
Do I bind something wrong inside my condition? Because if I remove the Rating-Binding, it works, but I don't see my mistake here.
There are multiple issues in your Condition bindings:
The first binding in the MultiDataTrigger uses a Condition that sets Property, which is wrong.
For a MultiDataTrigger, each condition in the collection must set both the Binding and Value properties. For more information, see Binding.
Set the Binding property using a RelativeSource binding to Self instead.
<Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True"/>
The second binding to Rating is faulty, since it would only ever set any star to be golden and opaque if the rating is exactly five stars. So no stars are selected for any other rating.
In order resolve these issues, you can use a simple Trigger on the IsChecked property.
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="star" Property="Fill" Value="Gold" />
<Setter TargetName="star" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
If it is really your requirement to only make the stars golden and opaque if the rating is exactly 5, then you could correct the MultiDataTrigger as stated above:
<ControlTemplate.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}" Value="True"/>
<Condition Binding="{Binding Rating, RelativeSource={RelativeSource FindAncestor, AncestorType=controls:RatingControl}}" Value="5"/>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter TargetName="star" Property="Fill" Value="Gold" />
<Setter TargetName="star" Property="Opacity" Value="1" />
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</ControlTemplate.Triggers>
Another note on value coercion. As #Clemens stated in the comments, setting a property in XAML e.g. like below will bypass your setter and
call SetValue directly. Your setter should only call SetValue as the behavior will differ otherwise when setting properties in XAML or through the property.
<local:RatingControl Rating="{Binding SomeProperty}"/>
Instead of the checks in the setter, you can specify a value coercion callback in the dependency property declaration.
public static readonly DependencyProperty RatingProperty = DependencyProperty.Register(
nameof(Rating), typeof(int), typeof(RatingControl), new FrameworkPropertyMetadata(
0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, RatingChanged, CoerceRating));
Then create a CoerceRating method that contains your checks and returns the corresponding values.
private static object CoerceRating(DependencyObject d, object baseValue)
{
var value = (int)baseValue;
if (value < 0)
{
return 0;
}
if (value > Max)
{
return Max;
}
return value;
}
Finally, remove all checks from the setter of Rating.
public int Rating
{
get => (int)GetValue(RatingProperty);
set => SetValue(RatingProperty, value);
}
The value coercion callback will automatically be called, when the property is set through SetValue and therefore ensure that the Rating value is within the valid interval.
I tried to build my a TextBox with some variable placeholder text.
But the property "PlaceholderText" ist not used in the Visual Brush - Label.
The placeholder text in <Label Content="{Binding PlaceholderText}" Foreground="LightGray" /> is always empty.
When I use the binding out of the , the binding works. I tried to use the binding as Text: <TextBox Text="{Binding PlaceholderText}" > and it works perfectly.
Why not inside the <VisualBrush><Label Content="...">...
I hope someone can help me.
<TextBox x:Class="CustomerPro.Common.Controls.PlaceholderTextBox"
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:CustomerPro.Common.Controls"
mc:Ignorable="d"
d:DesignHeight="50" d:DesignWidth="800" Name="phTextBox">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Resources>
<VisualBrush x:Key="PHBoxBackground" AlignmentX="Left" AlignmentY="Center" Stretch="None">
<VisualBrush.Visual>
<Label Content="{Binding PlaceholderText}" Foreground="LightGray" />
</VisualBrush.Visual>
</VisualBrush>
</Style.Resources>
<Style.Triggers>
<Trigger Property="Text" Value="">
<Setter Property="Background" Value="{StaticResource PHBoxBackground}" />
</Trigger>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Background" Value="{StaticResource PHBoxBackground}" />
</Trigger>
<Trigger Property="IsKeyboardFocused" Value="True">
<Setter Property="Background" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
public partial class PlaceholderTextBox : TextBox
{
#region PlaceholderText
/// <summary>
/// Gets or sets the PlaceholderText which is displayed next to the field
/// </summary>
public String PlaceholderText
{
get { return (String)GetValue(PlaceholderTextProperty); }
set { SetValue(PlaceholderTextProperty, value); }
}
/// <summary>
/// Identified the PlaceholderText dependency property
/// </summary>
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register("PlaceholderText", typeof(string), typeof(PlaceholderTextBox), new PropertyMetadata("Type some Text"));
#endregion
public PlaceholderTextBox()
{
InitializeComponent();
this.phTextBox.DataContext = this;
}
}
You need to bind by "ElementName" but that is more elaborated inside the Resources:
<Label
Content="{Binding Source={x:Reference phTextBox}, Path=PlaceholderText}"
Foreground="LightGray" />
See also msdn forum.
I have a an app that will open many windows, and I want all windows to look the same. I am overriding the default Windows window chrome style and making my own, so any new window that is opened (excluding messageboxes) should have the same window style. However, no matter what I seem to try it does not work. I can get it to work with one window, but when I want to make it a global style it always crashes or simply doesn't work as it should.
Here is my code:
WindowBaseStyle.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyProject.Styles"
xmlns:views="clr-namespace:Myproject.Views">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="GlobalStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="{x:Type views:WindowBase}" BasedOn="{StaticResource {x:Type Window}}">
<Setter Property="AllowsTransparency" Value="False" />
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="WindowState" Value="Normal" />
<Setter Property="WindowStyle" Value="SingleBorderWindow" />
<Setter Property="WindowChrome.WindowChrome">
<Setter.Value>
<WindowChrome CaptionHeight="30"
UseAeroCaptionButtons="False"/>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type views:WindowBase}">
<Border BorderBrush="Blue" BorderThickness="1" SnapsToDevicePixels="True">
<DockPanel Background="White" LastChildFill="True" >
<Grid Background="Blue" DockPanel.Dock="Top">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center">
<Button Name="PART_SystemMenuButton" Command="{Binding MenuCommand}" Style="{DynamicResource SystemIconButton}">
<Image Height="16" Width="16" Source="/Resources/icon.png" Stretch="Fill"/>
</Button>
<Viewbox Height="16" HorizontalAlignment="Stretch" Margin="14,2,0,0" >
<TextBlock FontSize="12" Foreground="White" Text="{Binding Title,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" />
</Viewbox>
</StackPanel>
<StackPanel Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True">
<Button x:Name="PART_MinimizeButton" Command="{Binding MinimizeCommand}" Height="30" Margin="0,0,0,0" ToolTip="Minimize" Width="45">
<Image Source="/Resources/minimize.png" Stretch="None" />
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="#0079CB" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#64AEEC"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="PART_MaximizeButton" Command="{Binding MaximizeCommand}" Height="30" Margin="0,0,0,0" Width="45">
<Image>
<Image.Style>
<Style TargetType="{x:Type Image}">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Normal">
<Setter Property="Source" Value="/Resources/maximize.png" />
<Setter Property="Stretch" Value="None" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Maximized">
<Setter Property="Source" Value="/Resources/unmaximize.png" />
<Setter Property="Stretch" Value="None" />
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="#0079CB" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#64AEEC"/>
</Trigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Normal">
<Setter Property="ToolTip" Value="Maximize" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}, Path=WindowState}" Value="Maximized">
<Setter Property="ToolTip" Value="Restore Down" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="PART_CloseButton" Command="{Binding CloseCommand}" Height="30" Margin="0,0,0,0" ToolTip="Close" Width="45" >
<Image Source="/Resources/close.png" Stretch="None" />
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="#0079CB" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Red"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</StackPanel>
</Grid>
<!-- this ContentPresenter automatically binds to the content of the window -->
<ContentPresenter />
</DockPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
WindowBase.cs
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
namespace MyProject.Views
{
[TemplatePart(Name = "PART_MinimizeButton", Type = typeof(Button))]
[TemplatePart(Name = "PART_MaximizeButton", Type = typeof(Button))]
[TemplatePart(Name = "PART_CloseButton", Type = typeof(Button))]
[TemplatePart(Name = "PART_SystemMenuButton", Type = typeof(Button))]
public class WindowBase: Window
{
static WindowBase()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomWindow), new FrameworkPropertyMetadata(typeof(CustomWindow)));
}
public WindowBase()
{
Loaded += (sender, evnt) =>
{
var MinimizeButton = (Button)Template.FindName("PART_MinimizeButton", this);
var MaximizeButton = (Button)Template.FindName("PART_MaximizeButton", this);
var CloseButton = (Button)Template.FindName("PART_CloseButton", this);
var SystemMenuButton = (Button)Template.FindName("PART_SystemMenuButton", this);
MinimizeButton.Click += (s, e) => WindowState = WindowState.Minimized;
MaximizeButton.Click += (s, e) => WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
CloseButton.Click += (s, e) => Close();
SystemMenuButton.Click += (s, e) => SystemCommands.ShowSystemMenu(this, GetMousePosition());
};
}
}
}
Window1.xaml
<local:WindowBase x:Class="MyProject.Views.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyProject.Views"
Height="750"
Width="1125">
<Grid>
</Grid>
</local:WindowBase>
Window1.xaml.cs
using System.Windows;
namespace MyProject.Views
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1: WindowBase
{
public Window1()
{
InitializeComponent();
}
}
}
I am overall following the MVVM pattern, and for the most part from all the articles and videos I have looked at online, they all follow this basic approach and they say it all works, but I can't seem to get it to work.
An additional note is that whenever I add my custom window control to the Window1.xaml file, it breaks the designer and says it is "Invalid Markup"
Also note I added my "WindowBaseStyle" resource dictionary to the App.xaml file as a merged resource dictionary.
Any help is greatly appreciated!! Thanks
Ok, as we discussed in the comments, it seems like the fastest solution to the problem you described is to use a StaticResource to get the window's style from a resource dictionary (or create an implicit style for windows.) I questioned the role of CustomWindow because I thought that might be causing problems with your default style override. (Remember: if you go the lookless control route and try to use the DefaultStyleKeyProperty override, you have to do this on every subclass of that control.)
However, I think doing something like this will get you reusable plumbing for multiple windows driven by viewmodels...
PopupHost
A class that would derive from your customized window. This code provides the following behaviors:
Allows the viewmodel to mark itself as having served its purpose, causing the window to close.
Allows attached properties to be specified by individual views that can affect the way the window appears on-screen, e.g. the window title.
Can be extended to notify presented items that the user has tried closing the window, allowing interception/cancellation or cleanup actions to be performed.
Code:
public class PopupHost : Window
{
private readonly AwaitableViewModelBase _viewModel;
public PopupHost(Window owner, AwaitableViewModelBase viewModel, string dataTemplateKey = null)
{
Owner = owner;
_viewModel = viewModel;
// Wrap the content in another presenter -- makes it a little easier to get to in order to look for attached properties.
var contentPresenter = new ContentPresenter
{
Content = viewModel
};
if (!string.IsNullOrWhiteSpace(dataTemplateKey))
contentPresenter.ContentTemplate = (DataTemplate) FindResource(dataTemplateKey);
Content = contentPresenter;
Task.Run(async () =>
{
await viewModel.Task;
Dispatcher.Invoke(Close);
});
Closed += ClosedHandler;
ApplyTemplate();
// Grab attached property values from the user control (or whatever element... you just need to find the descendant)
var contentElement = FindDescendantWithNonDefaultPropertyValue(contentPresenter, PopupWindowProperties.TitleProperty);
if (contentElement != null)
{
var binding = new Binding { Source = contentElement, Path = new PropertyPath(PopupWindowProperties.TitleProperty) };
SetBinding(TitleProperty, binding);
}
}
private void ClosedHandler(object sender, EventArgs args)
{
_viewModel?.Cancel();
Closed -= ClosedHandler;
}
private static Visual FindDescendant(Visual element, Predicate<Visual> predicate)
{
if (element == null)
return null;
if (predicate(element))
return element;
Visual foundElement = null;
(element as FrameworkElement)?.ApplyTemplate();
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
var visual = VisualTreeHelper.GetChild(element, i) as Visual;
foundElement = FindDescendant(visual, predicate);
if (foundElement != null)
break;
}
return foundElement;
}
private static Visual FindDescendantWithNonDefaultPropertyValue(Visual element, DependencyProperty dp)
{
return FindDescendant(element, e => !(dp.GetMetadata(e).DefaultValue ?? new object()).Equals(e.GetValue(dp)));
}
}
PopupWindowProperties
Just a dumb object containing solely attached properties so your views can convey some information to the window.
public static class PopupWindowProperties
{
public static readonly DependencyProperty TitleProperty = DependencyProperty.RegisterAttached("Title", typeof(string), typeof(PopupWindowProperties), new FrameworkPropertyMetadata(string.Empty));
public static void SetTitle(UIElement element, string value) => element.SetValue(TitleProperty, value);
public static string GetTitle(UIElement element) => element.GetValue(TitleProperty) as string;
}
AwaitableViewModelBase
A simple abstract viewmodel which has a TaskCompletionSource. This allows the popup window and the viewmodel to coordinate closing.
public abstract class AwaitableViewModelBase : ViewModelBase
{
protected TaskCompletionSource<bool> TaskCompletionSource { get; set; }
public Task<bool> Task => TaskCompletionSource?.Task;
public void RegisterTaskCompletionSource(TaskCompletionSource<bool> tcs)
{
var current = TaskCompletionSource;
if (current != null && current.Task.Status == TaskStatus.Running)
throw new InvalidOperationException();
TaskCompletionSource = tcs;
}
public virtual void Cancel() => SetResult(false);
protected void SetResult(bool result) => TaskCompletionSource?.TrySetResult(result);
}
WindowService
Last but not least, the simple service that can present the requested view and viewmodel. You can use implicit DataTemplates for your viewmodels, or provide the specific x:Key value of the template you wish to use. Note that the await doesn't really do anything here because ShowDialog blocks. We return the bool since it can be used to easily identify if the user hit OK or Cancel on a modal.
public class WindowService
{
public async Task<bool> ShowModalAsync(AwaitableViewModelBase viewModel, string dataTemplateKey = null)
{
var tcs = new TaskCompletionSource<bool>();
viewModel.RegisterTaskCompletionSource(tcs);
Application.Current.Dispatcher.Invoke(() =>
{
var currentWindow = Application.Current.Windows.OfType<Window>().SingleOrDefault(x => x.IsActive) ?? Application.Current.MainWindow;
var window = new PopupHost(currentWindow, viewModel, dataTemplateKey);
window.ShowDialog();
});
return await viewModel.Task;
}
}
The goal is to check the state of parameters, the state of each parameter can take the enum value (lock, unlock, or valueIncorrect). The display
will be different according to the state of the parameter(e.g, lock rectangle will be red, unlock the text will be bold and vallueIncorrect will be gray).
In main class we have different parameters VarA, VarB and VarC for example. The state of each parameter can be different.
enum State{lock, unlock, valueIncorrect};
State VarA = lock
State VarB = unlock
State VarC = lock
before I was this style and a rectangle based on this style and When I pressed a button , the style will change, but the reaction will be the same on all parameter
<Style x:Key="DisplayLockGroup" TargetType="GroupBox">
<Setter Property="BorderThickness" Value="2"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Source={x:Static local:LockMgt.Instance}, Path=isLocked}" Value="true">
<Setter Property="BorderBrush" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Source={x:Static local:LockMgt.Instance}, Path=isLocked}" Value="false">
<Setter Property="BorderBrush" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
<Rectangle x:Name="r_LockEcuTypes" Grid.Row="5" Grid.RowSpan="3" Grid.Column ="1" Grid.ColumnSpan="7" Style="{DynamicResource DisplayLockRectangleGroup}" />
Now i would like to change this behavior, I have different parameter and I would like to individually check the state of this parameter and apply one style according to the value, but i don't know How I can do it.
E.g for one parameter:
State VarA=lock => i would like to apply the style1
State VarB=unlock => i would like to apply the style2
State VarC=lock => i would like to apply the style1 but if the value change from lock to unlock, I would like to apply the style2
I don't know how create the XAML to display correctly what is expected.
As commented, depending on your actual needs, you have several options.
Converter from State to some property value
Change the properties via Style.Triggers (similar to your current approach)
Wrap your control and exchange inner styles via ControlTemplate.Triggers on the wrapper control
The following example is presenting three rectangles (rect1, rect2 and rect3 representing the approach with the same number) and a button that is responsible for changing the State of the ViewModel object in order to show how each rectangle reacts.
The XAML
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525"
Loaded="Window_Loaded">
<Window.Resources>
<!-- Used for 1. -->
<local:StateToBorderBrushConverter x:Key="stateConverter"/>
<!-- Used for 2. -->
<Style x:Key="DisplayLockGroup" TargetType="Rectangle">
<Setter Property="StrokeThickness" Value="2"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Data1State}" Value="{x:Static local:State.locked}">
<Setter Property="Stroke" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Data1State}" Value="{x:Static local:State.unlock}">
<Setter Property="Stroke" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
<!-- Used for 3. -->
<Style x:Key="DefaultRectangleStyle" TargetType="Rectangle">
<Setter Property="StrokeThickness" Value="2"/>
</Style>
<Style x:Key="LockedRectangleStyle" TargetType="Rectangle">
<Setter Property="StrokeThickness" Value="2"/>
<Setter Property="Stroke" Value="Red" />
</Style>
<Style x:Key="UnlockedRectangleStyle" TargetType="Rectangle">
<Setter Property="StrokeThickness" Value="2"/>
<Setter Property="Stroke" Value="Green" />
</Style>
</Window.Resources>
<Grid x:Name="grid1">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Rectangle x:Name="rect1" Grid.Row="0" Margin="5" StrokeThickness="2" Stroke="{Binding Data1State,Converter={StaticResource stateConverter}}" />
<Rectangle x:Name="rect2" Grid.Row="1" Margin="5" Style="{StaticResource DisplayLockGroup}" />
<ContentControl Margin="5" Grid.Row="2">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Rectangle x:Name="rect3" Style="{StaticResource DefaultRectangleStyle}" />
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Data1State}" Value="{x:Static local:State.locked}">
<Setter TargetName="rect3" Property="Style" Value="{StaticResource LockedRectangleStyle}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Data1State}" Value="{x:Static local:State.unlock}">
<Setter TargetName="rect3" Property="Style" Value="{StaticResource UnlockedRectangleStyle}"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
<Button x:Name="ChangeStateButton" Grid.Row="3" Margin="5" VerticalAlignment="Center" Content="Change State" Click="ChangeStateButton_Click"/>
</Grid>
</Window>
The Code
namespace WpfApplication2
{
public enum State
{
locked,
unlock,
valueIncorrect
}
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([CallerMemberName] string prop = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(prop));
}
}
}
public class MyData : BaseViewModel
{
private State _Data1State;
public State Data1State
{
get { return _Data1State; }
set
{
if (_Data1State != value)
{
_Data1State = value;
NotifyPropertyChanged();
}
}
}
}
public class StateToBorderBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is State)
{
var s = (State)value;
switch (s)
{
case State.locked:
return new SolidColorBrush(Colors.Red);
case State.unlock:
return new SolidColorBrush(Colors.Green);
case State.valueIncorrect:
return new SolidColorBrush(Colors.White);
default:
break;
}
}
return new SolidColorBrush(Colors.Transparent);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private MyData ContextData;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
ContextData = new MyData { Data1State = State.locked };
grid1.DataContext = ContextData;
}
private void ChangeStateButton_Click(object sender, RoutedEventArgs e)
{
switch (ContextData.Data1State)
{
case State.locked:
ContextData.Data1State = State.unlock;
break;
case State.unlock:
ContextData.Data1State = State.valueIncorrect;
break;
case State.valueIncorrect:
ContextData.Data1State = State.locked;
break;
default:
ContextData.Data1State = State.locked;
break;
}
}
}
}
Pros and cons:
The converter approach is useful, if the same property type is targeted in many different control types. The converter is returning a Brush and it doesn't care whether that brush will be used in a Rectangle.Stroke, a Border.BorderBrush or some other context.
The style trigger approach is less flexible regarding the targeted control type, but it is easier to maintain changes of more than a single property based on the status.
The control template trigger approach is useful in some advanced scenarios. It allows separate definition of the styles for each state. However, I'd only recommend it if you actually derive your own custom control with additional functionality, not as an ad-hoc control template just for switching styles.
You can apply an enum to color converter in the XAML binding
Background="{Binding Source=StateValue,
Converter={StaticResource stateValueColorConverter}}"
You'll easily find the detailed documentation: this is to give you the main idea of the usage.
Thanks to grek40, it is exactly what I want. I have try the option 3 and 2 and it works fine, I have use the case 2, it is most easily to implement.
I have use the same behavior for add an image on lock and unlock state.
In the Xaml.cs file, How I can access to parameter rect3 on your proposal, when I try it I have compilation error "the name "rect3" does not exist in the current view, I have written
XAML.CS :
rect3.Visibility = Visibility.Collapsed;
XAML :
<ContentControl Grid.RowSpan="3" Grid.ColumnSpan="3" KeyboardNavigation.IsTabStop="False">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Rectangle x:Name="rect3" Style="{StaticResource DefaultRectangleStyle}" />
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Data1State}" Value="{x:Static local:State.locked}">
<Setter TargetName="rect3" Property="Style" Value="{StaticResource LockedRectangleStyle}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Data1State}" Value="{x:Static local:State.unlock}">
<Setter TargetName="rect3" Property="Style" Value="{StaticResource UnlockedRectangleStyle}"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
I have a border in WPF that I am trying to enable or disable based on two properties in the viewmodel: ConnectedVisibility and OperatingMode. This data trigger disables the border when connectedvisibility visibility is not set to "Visible". But it does not work for OperatingMode. For OperatingMode other than 0, the border should be disabled but it stays enabled. It appears that there is no impact of OperatingMode changing its value at all. Even the breakpoints that I put in the convertor are not being hit except when the program first starts up. The bindings look Ok as there is no problem shown in Debug output for these bindings. Any help is appreciated.
The Style is
<Style x:Key="EnableOnConnectBorderCorrected" TargetType="{x:Type Border}">
<!--<Setter Property="BorderBrush" Value="#FFDADADA"/>-->
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="2"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ConnectedVisibility}" Value="Visible"/>
<Condition Binding="{Binding OperatingMode}" Value="0"/>
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True"/>
</MultiDataTrigger>
<DataTrigger Binding="{Binding OperatingMode, Converter={x:Static VM:IsEqualOrGreaterThanSHORTConverter.Instance}, ConverterParameter=1,Mode=TwoWay}" Value="True">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
<DataTrigger Binding="{Binding ConnectedVisibility}" Value="Collapsed">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
The convertor used in the style is ->
public class IsEqualOrGreaterThanSHORTConverter : IValueConverter
{
public static readonly IValueConverter Instance = new IsEqualOrGreaterThanSHORTConverter();
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
short iValue = (short)value;
short compareToValue = System.Convert.ToInt16(parameter);
return iValue >= compareToValue;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
The XAML where this style is applied is
<Border Name="RebootDash" Grid.Row="2" Grid.Column="1" Style="{StaticResource EnableOnConnectBorderCorrected}" BorderBrush="#FFDADADA" BorderThickness="1" CornerRadius="2" Width="Auto" Margin="0,1,1,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="2" Background="Wheat"/>
<telerik:RadButton Command="{Binding ResetUnitCommand, Source={StaticResource UnitCommandProvider}}" Style="{StaticResource DashBoardImageButton}">
<Image Grid.Row="0" Source="/Images/UnitManagementImages/IMG_THOR_UNITResetUnit128.png"
ToolTip="{x:Static properties:Resources.Unit_Command_ResetUnit}"
Width="40" Height="40"
Margin="0,5,0,5"
HorizontalAlignment="Center"/>
</telerik:RadButton>
<TextBlock Grid.Row="2" Text="{x:Static properties:Resources.Unit_Command_ResetUnit}" HorizontalAlignment="Center" Margin="5,5,5,5"/>
</Grid>
</Border>
The properties to which it is bound are
public Visibility ConnectedVisibility
{
get { return connectedVisibility; }
set
{
if (connectedVisibility == value) return;
connectedVisibility = value;
RaisePropertyChanged("ConnectedVisibility");
}
}
public short OperatingMode
{
get { return UnitOperatingModeVM.OperatingMode; }
set
{
UnitOperatingModeVM.OperatingMode = value;
}
}
since you have only one condition to enable the same, so perhaps setting IsEnabled to False by default should do the trick
<Style x:Key="EnableOnConnectBorderCorrected" TargetType="{x:Type Border}">
<!--<Setter Property="BorderBrush" Value="#FFDADADA"/>-->
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="2"/>
<Setter Property="IsEnabled" Value="False"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ConnectedVisibility}" Value="Visible"/>
<Condition Binding="{Binding OperatingMode}" Value="0"/>
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
I have added <Setter Property="IsEnabled" Value="False"/> in the style which will by default disable the control and I have removed the other redundant conditions
so as result when the both conditions in MultiDataTrigger will meet it will enable the same, otherwise it remains disabled
above example assumes that both of the property in MultiDataTrigger ConnectedVisibility & OperatingMode are notifying the changes.
you may also need to add the notification for OperatingMode in order for MultiDataTrigger to fire the trigger
public short OperatingMode
{
get { return UnitOperatingModeVM.OperatingMode; }
set
{
UnitOperatingModeVM.OperatingMode = value;
RaisePropertyChanged("OperatingMode");
}
}
Is your OperatingMode property set implementation call RaisePropertyChanged("OperatingMode");
private short operatingMode;
public short OperatingMode
{
get
{
return operatingMode;
}
set
{
if (operatingMode != value)
{
operatingMode = value;
this.RaisePropertyChanged("OperatingMode");
}
}
}