Why ScrollBar's ValueChanged EventTrigger is binding animation to previous value - wpf

I have a strange behavior in my WPF application.
I am trying to launch an thickness animation on ScrollBar.ValueChanged event for animating an element margin, but every time the scroll bar value changed, the animation is binded to the OLD value of the scroll bar.
Resulting in a gap between the scroll bar value and the margin element.
<UserControl.Resources>
<Storyboard x:Key="AnimationScrollTest">
<ThicknessAnimation Storyboard.TargetName="ElementTest" Storyboard.TargetProperty="Margin" Duration="0:0:0:0.3"
To="{Binding ElementName=ScrollBarTest, Path=Value, Converter={StaticResource MyVerticalScrollBarValueToMarginConverter}}" />
</Storyboard>
<UserControl.Resources>
<Grid>
<ScrollBar x:Name="ScrollBarTest" Grid.RowSpan="3" Grid.ColumnSpan="2" Orientation="Vertical" Right" VerticalAlignment="Stretch" SmallChange="10" LargeChange="100" Value="0" Maximum="2000" >
<ScrollBar.Triggers>
<EventTrigger RoutedEvent="ScrollBar.ValueChanged" >
<BeginStoryboard Storyboard="{StaticResource AnimationScrollTest}" />
</EventTrigger>
</ScrollBar.Triggers>
</ScrollBar>
<Border x:Name="ElementTest" Grid.RowSpan="3" Grid.ColumnSpan="2" HorizontalAlignment="Center" VerticalAlignment="Top" Width="50" Height="50" Background="Red"></Border>
</Grid>
public class VerticalScrollBarValueToMarginConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return new Thickness(0, (Double)value, 0, 0);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
When the EventTrigger is linked to ScrollBar.Scroll event, it works well.
When a breakpoint is added in the converter, the value object in the Convert method have the correct value
If the ElementTest margin is directly binded to the ScrollBar value (without animation), it works well too.
Any idea??
Thanks a lot
PS: sorry for the bad english, I'm french!

I attempted to solve the previous value problem with attached properties
xaml
<Grid xmlns:l="clr-namespace:CSharpWPF">
<ScrollBar x:Name="ScrollBarTest"
Grid.RowSpan="3"
Grid.ColumnSpan="2"
Orientation="Vertical"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
SmallChange="10"
LargeChange="100"
Value="0"
Maximum="2000">
</ScrollBar>
<Border x:Name="ElementTest"
Grid.RowSpan="3"
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="50"
Height="50"
Background="Red"
l:ScrollBarToMarginAnimator.ScrollBar="{Binding ElementName=ScrollBarTest}"></Border>
</Grid>
I have removed the storyboard from here and added an attached property l:ScrollBarToMarginAnimator.ScrollBar to the target element and binded to the scrollbar ScrollBarTest
class for the attached property
namespace CSharpWPF
{
public class ScrollBarToMarginAnimator : DependencyObject
{
public static ScrollBar GetScrollBar(DependencyObject obj)
{
return (ScrollBar)obj.GetValue(ScrollBarProperty);
}
public static void SetScrollBar(DependencyObject obj, ScrollBar value)
{
obj.SetValue(ScrollBarProperty, value);
}
// Using a DependencyProperty as the backing store for ScrollBar. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ScrollBarProperty =
DependencyProperty.RegisterAttached("ScrollBar", typeof(ScrollBar), typeof(ScrollBarToMarginAnimator), new PropertyMetadata(null, OnScrollBarChanged));
private static void OnScrollBarChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollBar sb = e.NewValue as ScrollBar;
if (sb != null)
sb.Scroll += (ss, ee) =>
{
ThicknessAnimation ta = new ThicknessAnimation(new Thickness(0, sb.Value, 0, 0), TimeSpan.FromMilliseconds(300));
(d as FrameworkElement).BeginAnimation(FrameworkElement.MarginProperty, ta);
};
}
}
}
in this class I am listening to the Scroll event of the binded scrollbar and initiating a ThicknessAnimation on the attached element's Margin property.
above solution will listen any change in scroll bar and will react accordingly by animating margin of the border.
Multi Scrollbar
xaml
<Grid xmlns:l="clr-namespace:CSharpWPF">
<ScrollBar x:Name="vertical"
Grid.RowSpan="3"
Grid.ColumnSpan="2"
Orientation="Vertical"
HorizontalAlignment="Right"
VerticalAlignment="Stretch"
SmallChange="10"
LargeChange="100"
Value="0"
Maximum="2000"/>
<ScrollBar x:Name="horizontal"
Grid.RowSpan="3"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
SmallChange="10"
LargeChange="100"
Value="0"
Maximum="2000"/>
<Border x:Name="ElementTest"
Grid.RowSpan="3"
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Width="50"
Height="50"
Background="Red"
l:ScrollBarToMarginAnimator.Vertical="{Binding ElementName=vertical}"
l:ScrollBarToMarginAnimator.Horizontal="{Binding ElementName=horizontal}"/>
</Grid>
I have added another scrollbar and attached two properties to the border.
cs
namespace CSharpWPF
{
public class ScrollBarToMarginAnimator : DependencyObject
{
public static ScrollBar GetVertical(DependencyObject obj)
{
return (ScrollBar)obj.GetValue(VerticalProperty);
}
public static void SetVertical(DependencyObject obj, ScrollBar value)
{
obj.SetValue(VerticalProperty, value);
}
// Using a DependencyProperty as the backing store for Vertical. This enables animation, styling, binding, etc...
public static readonly DependencyProperty VerticalProperty =
DependencyProperty.RegisterAttached("Vertical", typeof(ScrollBar), typeof(ScrollBarToMarginAnimator),
new PropertyMetadata(null, (d, e) => AttachAnimation(d, e, true)));
public static ScrollBar GetHorizontal(DependencyObject obj)
{
return (ScrollBar)obj.GetValue(HorizontalProperty);
}
public static void SetHorizontal(DependencyObject obj, ScrollBar value)
{
obj.SetValue(HorizontalProperty, value);
}
// Using a DependencyProperty as the backing store for Horizontal. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HorizontalProperty =
DependencyProperty.RegisterAttached("Horizontal", typeof(ScrollBar), typeof(ScrollBarToMarginAnimator),
new PropertyMetadata(null, (d, e) => AttachAnimation(d, e, false)));
private static void AttachAnimation(DependencyObject d, DependencyPropertyChangedEventArgs e, bool isVertical)
{
ScrollBar sb = e.NewValue as ScrollBar;
if (sb != null)
sb.Scroll += (ss, ee) =>
{
FrameworkElement fw = d as FrameworkElement;
Thickness newMargin = fw.Margin;
if (isVertical)
newMargin.Top = sb.Value;
else
newMargin.Left = sb.Value;
ThicknessAnimation ta = new ThicknessAnimation(newMargin, TimeSpan.FromMilliseconds(300));
fw.BeginAnimation(FrameworkElement.MarginProperty, ta);
};
}
}
}
I have created two properties and attached the animation using a flag

Related

How to defeat a bug with Grid.Row / Column in ItemsPanelTemplate?

Created a simple Attached property to simplify bindings from an element template.
Instead of this:
<ItemsControl ItemsSource="{Binding Mode=OneWay, Source={StaticResource Points.Grid}}"
ItemsPanel="{StaticResource Grid.Panel}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="Point">
<Ellipse Fill="Coral"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Row" Value="{Binding Y}"/>
<Setter Property="Grid.Column" Value="{Binding X}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
You can go this way:
<ItemsControl Grid.Row="1"
ItemsSource="{Binding Mode=OneWay, Source={StaticResource Points.Grid}}"
ItemsPanel="{StaticResource Grid.Panel}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="Point">
<Ellipse Fill="LightBlue"
pa:Grid.Row="{Binding Y}"
pa:Grid.Column="{Binding X}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here is the complete code the attached property:
public static partial class Grid
{
public static int GetRow(FrameworkElement element)
{
return (int)element.GetValue(RowProperty);
}
public static void SetRow(FrameworkElement element, int value)
{
element.SetValue(RowProperty, value);
}
// Using a DependencyProperty as the backing store for Row. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RowProperty =
DependencyProperty.RegisterAttached("Row", typeof(int), typeof(Grid),
new FrameworkPropertyMetadata
(
0,
FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
RowChanged
));
private static void RowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element))
throw new ArgumentException("Must be FrameworkElement", nameof(d));
FrameworkElement parent;
while ((parent = VisualTreeHelper.GetParent(element) as FrameworkElement) != null && !(parent is System.Windows.Controls.Grid))
element = parent;
if (parent is System.Windows.Controls.Grid grid)
element.SetValue(System.Windows.Controls.Grid.RowProperty, (int)e.NewValue);
}
private static void GridLoaded(object sender, RoutedEventArgs e)
=> ((System.Windows.Controls.Grid)sender).InvalidateMeasure();
public static int GetColumn(FrameworkElement element)
{
return (int)element.GetValue(ColumnProperty);
}
public static void SetColumn(FrameworkElement element, int value)
{
element.SetValue(ColumnProperty, value);
}
// Using a DependencyProperty as the backing store for Column. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnProperty =
DependencyProperty.RegisterAttached("Column", typeof(int), typeof(Grid),
new FrameworkPropertyMetadata
(
0,
FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
ColumnChanged
));
private static void ColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element))
throw new ArgumentException("Must be FrameworkElement", nameof(d));
FrameworkElement parent;
while ((parent = VisualTreeHelper.GetParent(element) as FrameworkElement) != null && !(parent is System.Windows.Controls.Grid))
element = parent;
if (parent is System.Windows.Controls.Grid grid)
element.SetValue(System.Windows.Controls.Grid.ColumnProperty, (int)e.NewValue);
}
}
The property is nothing complicated.
With Canvas, the same works fine.
And with the Grid, problems arise when attaching a collection with elements or when adding the first element to the collection - elements are displayed in the Grid without regard to their position.
Although when viewing in the visual tree and in the properties browser, the attached properties of Grid.Row / Column are set correctly.
And at the slightest change in the window, the elements fall into place.
In my opinion, a frank bug.
But how to deal with it?
Full XAML Demo Code:
<Window x:Class="AttachedPropertiesWPF.BindParentWind"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AttachedPropertiesWPF"
mc:Ignorable="d"
Title="BindParentWind" Height="450" Width="800"
xmlns:pa="clr-namespace:AttachedProperties;assembly=AttachedProperties">
<Window.Resources>
<x:Array x:Key="Points.Grid" Type="Point">
<Point X="1" Y="0"/>
<Point X="0" Y="2"/>
<Point X="2" Y="1"/>
</x:Array>
<ItemsPanelTemplate x:Key="Grid.Panel">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
</Grid>
</ItemsPanelTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding Mode=OneWay, Source={StaticResource Points.Grid}}"
ItemsPanel="{StaticResource Grid.Panel}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="Point">
<Ellipse Fill="Coral"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Row" Value="{Binding Y}"/>
<Setter Property="Grid.Column" Value="{Binding X}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
<ItemsControl Grid.Row="1"
ItemsSource="{Binding Mode=OneWay, Source={StaticResource Points.Grid}}"
ItemsPanel="{StaticResource Grid.Panel}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="Point">
<Ellipse Fill="LightBlue"
pa:Grid.Row="{Binding Y}"
pa:Grid.Column="{Binding X}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
Outputs like this:
And it should be like this:
It seems to be kind of a timing issue that initially setting Grid.Row and Grid.Column on the ContentPresenter won't result in another layout cycle.
While it would obviously be a good idea to drop the whole helper class and set the Grid properties directly in an ItemContainerStyle, an ugly workaround would be to asynchronously set the Grid properties, like shown here:
public class ContentPresenterHelper
{
public static readonly DependencyProperty ColumnProperty =
DependencyProperty.RegisterAttached(
"Column", typeof(int), typeof(ContentPresenterHelper),
new PropertyMetadata(0, ColumnPropertyChanged));
public static readonly DependencyProperty RowProperty =
DependencyProperty.RegisterAttached(
"Row", typeof(int), typeof(ContentPresenterHelper),
new PropertyMetadata(0, RowPropertyChanged));
public static int GetRow(DependencyObject o)
{
return (int)o.GetValue(RowProperty);
}
public static void SetColumn(DependencyObject o, int value)
{
o.SetValue(ColumnProperty, value);
}
public static int GetColumn(DependencyObject o)
{
return (int)o.GetValue(ColumnProperty);
}
public static void SetRow(DependencyObject o, int value)
{
o.SetValue(RowProperty, value);
}
private static void ColumnPropertyChanged(
DependencyObject o, DependencyPropertyChangedEventArgs e)
{
o.Dispatcher.InvokeAsync(() =>
FindContentPresenterParent(o)?.SetValue(
Grid.ColumnProperty, (int)e.NewValue));
}
private static void RowPropertyChanged(
DependencyObject o, DependencyPropertyChangedEventArgs e)
{
o.Dispatcher.InvokeAsync(() =>
FindContentPresenterParent(o)?.SetValue(
Grid.RowProperty, (int)e.NewValue));
}
private static ContentPresenter FindContentPresenterParent(DependencyObject element)
{
if (element == null)
{
return null;
}
var parent = VisualTreeHelper.GetParent(element);
return (parent as ContentPresenter) ?? FindContentPresenterParent(parent);
}
}
Welcome to SO!
I'll be honest, there are many, many problems with this code, but I'll stick to what you've posted....
Clemens is correct, it looks like you're a bit confused as to how you should be positioning your elements on the grid. In your first ItemsControl you're doing it via the ItemContainerStyle, in the second one you're applying it directly to the Ellipse (albeit, confusingly, using your custom Grid helper DPs). What you do to the first control won't affect the second one, so of course the layout behavior you see will be different between them as well.
Item templates such as your ellipse don't get added to the parent panel container directly, they get encapsulated in a ContentPresenter. So your first control is doing it properly. Set the ItemContainerStyle in your second ItemsControl as well, remove the ap:Grid.Row and ap:Grid.Column setters from the Ellipse tag in your second control, and get rid of that Grid helper class altogether, you don't need it.
Working implementation of Attached Properties:
public static partial class Grid
{
public static int GetRow(FrameworkElement element)
{
return (int)element.GetValue(RowProperty);
}
public static void SetRow(FrameworkElement element, int value)
{
element.SetValue(RowProperty, value);
}
// Using a DependencyProperty as the backing store for Row. This enables animation, styling, binding, etc...
public static readonly DependencyProperty RowProperty =
DependencyProperty.RegisterAttached("Row", typeof(int), typeof(Grid),
new FrameworkPropertyMetadata
(
0,
FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsParentMeasure
| FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure
| FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
RowChanged
));
private static void RowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element))
throw new ArgumentException("Must be FrameworkElement", nameof(d));
FrameworkElement parent;
while ((parent = VisualTreeHelper.GetParent(element) as FrameworkElement) != null && !(parent is System.Windows.Controls.Grid))
element = parent;
if (parent is System.Windows.Controls.Grid grid)
element.Dispatcher.BeginInvoke((Action<FrameworkElement, DependencyProperty, object>)SetValueAsync, element, System.Windows.Controls.Grid.RowProperty, (int)e.NewValue);
}
private static void SetValueAsync(FrameworkElement element, DependencyProperty property, object value)
=> element.SetValue(property, value);
public static int GetColumn(FrameworkElement element)
{
return (int)element.GetValue(ColumnProperty);
}
public static void SetColumn(FrameworkElement element, int value)
{
element.SetValue(ColumnProperty, value);
}
// Using a DependencyProperty as the backing store for Column. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnProperty =
DependencyProperty.RegisterAttached("Column", typeof(int), typeof(Grid),
new FrameworkPropertyMetadata
(
0,
FrameworkPropertyMetadataOptions.AffectsParentArrange | FrameworkPropertyMetadataOptions.AffectsParentMeasure
| FrameworkPropertyMetadataOptions.AffectsArrange | FrameworkPropertyMetadataOptions.AffectsMeasure
| FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
ColumnChanged
));
private static void ColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is FrameworkElement element))
throw new ArgumentException("Must be FrameworkElement", nameof(d));
FrameworkElement parent;
while ((parent = VisualTreeHelper.GetParent(element) as FrameworkElement) != null && !(parent is System.Windows.Controls.Grid))
element = parent;
if (parent is System.Windows.Controls.Grid grid)
element.Dispatcher.BeginInvoke((Action<FrameworkElement, DependencyProperty, object>)SetValueAsync, element, System.Windows.Controls.Grid.ColumnProperty, (int)e.NewValue);
}
}
I have not figured out the reasons for the incomprehensible behavior of the basу properties Grid.Row/Column.
Later I will watch their source code.
If I understand the reason, I'll post it here.

how to make a dependency property of type bool a two way binding

Can anyone help me with this problem , because i read a blog how to do this on
Walkthrough: Two-way binding inside a XAML User Control
but i don't now how to do this with a bool value
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValueIsSelected(IsSelectedProperty, value); }
}
private void SetValueIsSelected(DependencyProperty property, object value,
[System.Runtime.CompilerServices.CallerMemberName] bool s = null)
{
SetValue(property, value);
if (PropertyChanged != null)
{
string sender = s.ToString();
PropertyChanged(this, new PropertyChangedEventArgs(sender));
}
}
// Using a DependencyProperty as the backing store for IsSelected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool), typeof(CustomPolygon), new PropertyMetadata(0));
So this was wrong accordingly #Clemens
to understand it more here i some more information on my application
In my MainWindow i am using two ComboBoxes to fill my data with binding to a public ObservableCollection DataPlannen
My code behind MainWindow:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private ObservableCollection<Plan> dataPlannen;
public ObservableCollection<Plan> DataPlannen
{
get { return dataPlannen; }
set
{
if (value != dataPlannen)
{
dataPlannen = value;
}
}
}
//non relevant code deleted
//get database data for ComboBoxes
private void btnGetPlanData_Click(object sender, RoutedEventArgs e)
{
try
{
this.Cursor = Cursors.Wait;
DataPlannen = new PlanCanvasModel().PlanHotspotsHardwares;
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString();
}
finally
{
this.Cursor = Cursors.Arrow;
}
}
private void cmbHotspot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (cmbHotspot.SelectedIndex != -1)
{
foreach (Hotspot hotspot in cmbHotspot.ItemsSource)
{
if (hotspot == (Hotspot)cmbHotspot.SelectedItem)
{
hotspot.IsSelected = true;
else
{
hotspot.IsSelected = false;
}
}
}
}
My MainWindow XAML:
<Grid Background="LightGray">
<DockPanel Name="TestCanvas" LastChildFill="True">
<Grid x:Name="Sidebar" DockPanel.Dock="Right" Width="200">
<StackPanel Margin="0,10,10,0">
<ScrollViewer Height="112" VerticalScrollBarVisibility="Auto">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
<Button x:Name="btnGetPlanData" Width="30" Height="30" HorizontalAlignment="Left" Margin="5,0" Click="btnGetPlanData_Click">
<Image Source="Images/database38.png" Stretch="Uniform"></Image>
<Button.ToolTip>Laad plannen in</Button.ToolTip>
</Button>
</StackPanel>
<Grid>
<ComboBox x:Name="cmbPlannen" Width="180"
ItemsSource="{Binding ElementName=myWindow,Path=DataPlannen}"
DisplayMemberPath="{Binding Plan_naam}"
SelectedValuePath="{Binding Plan_Id}"
SelectionChanged="cmbPlannen_SelectionChanged" IsEditable="True">
<ComboBox.ToolTip>
<ToolTip>Zoek op text of selecteer</ToolTip>
</ComboBox.ToolTip>
</ComboBox>
</Grid>
</StackPanel>
</ScrollViewer>
<Grid>
<ComboBox Margin="5" SnapsToDevicePixels="True" ItemsSource="{Binding ElementName=cmbPlannen,Path=SelectedItem.Hotspots,Mode=TwoWay}"
x:Name="cmbHotspot" SelectedIndex="0"
DisplayMemberPath="{Binding Hotspot_naam}"
SelectedValuePath="{Binding Hotspot_Id}"
SelectedItem="{Binding SelectedItem}"
SelectionChanged="cmbHotspot_SelectionChanged" IsEditable="True">
<ComboBox.ItemContainerStyle>
<Style>
<Setter Property="Control.Padding" Value="0"></Setter>
<Style.Triggers>
<Trigger Property="ComboBoxItem.IsSelected" Value="True">
<Setter Property="ComboBoxItem.Background" Value="LightGray" />
</Trigger>
<Trigger Property="ComboBoxItem.IsHighlighted" Value="True">
<Setter Property="ComboBoxItem.Background" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</ComboBox.ItemContainerStyle>
<ComboBox.ToolTip>
<ToolTip>Zoek op text of selecteer</ToolTip>
</ComboBox.ToolTip>
</ComboBox>
</Grid>
</StackPanel>
</Grid>
<Grid x:Name="MainContainer" ClipToBounds="True"
>
<Viewbox>
<ItemsControl x:Name="drawingsheet" ItemsSource="{Binding ElementName=cmbPlannen, Path=SelectedItem.Hotspots}"
Width="{StaticResource canvasWidth}"
Height="{StaticResource canvasHeight}"
MouseLeftButtonUp="drawingsheet_MouseLeftButtonUp"
MouseRightButtonDown="drawingsheet_MouseRightButtonDown"
MouseWheel="drawingsheet_MouseWheel"
MouseLeftButtonDown="drawingsheet_MouseLeftButtonDown"
MouseMove="drawingsheet_MouseMove"
Background="White"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="{Binding ElementName=cmbPlannen,Path=SelectedItem.Plan_image,Converter={StaticResource ResourceKey=bytesToBitmapImageConverter}}"
Width="{StaticResource canvasWidth}" Height="{StaticResource canvasHeight}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<customPolygon:CustomPolygon x:Name="currentPolygon" PointsPolygon="{Binding Hotspot_points}"
IsSelected="{Binding IsSelected,Mode=TwoWay}"
HasChildren="{Binding HasChildren}"></customPolygon:CustomPolygon>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
<Setter Property="Canvas.Left" Value="{Binding Path=X}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="scaleCanvas"></ScaleTransform>
<TranslateTransform x:Name="moveCanvas"></TranslateTransform>
</TransformGroup>
</ItemsControl.RenderTransform>
</ItemsControl>
</Viewbox>
</Grid>
</DockPanel>
</Grid>
My UserControl CustomPolygon.Xaml:
<UserControl x:Class="testCanvas.Controls.DrawingControls.CustomPolygon"
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:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
Name="userControl"
>
<Polygon x:Name="polygon"
Points="{Binding ElementName=userControl,Path=PointsSource}"
StrokeThickness="0.5"
Stroke="Black"
Opacity="0.5"
MouseEnter="polygon_MouseEnter"
MouseLeave="polygon_MouseLeave"
MouseLeftButtonDown="polygon_MouseLeftButtonDown"/>
</UserControl>
My code behind from CustomPolygon
public partial class CustomPolygon : UserControl, INotifyPropertyChanged
{
public CustomPolygon()
{
InitializeComponent();
}
#region Properties
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set
{
SetValue(IsSelectedProperty, value);
if (IsSelected)
{
polygon.Stroke = Brushes.Red;
polygon.StrokeThickness = 2;
}
else
{
polygon.Stroke = Brushes.Black;
polygon.StrokeThickness = 0.5;
}
}
}
// Using a DependencyProperty as the backing store for IsSelected. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register("IsSelected", typeof(bool), typeof(CustomPolygon),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
//points for polygon
public PointCollection PointsPolygon
{
get { return (PointCollection)GetValue(PointsPolygonProperty); }
set { SetValue(PointsPolygonProperty, value); }
}
// Using a DependencyProperty as the backing store for PointsPolygon. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PointsPolygonProperty =
DependencyProperty.Register("PointsPolygon", typeof(PointCollection), typeof(CustomPolygon),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public bool HasChildren
{
get { return (bool)GetValue(HasChildrenProperty); }
set { SetValue(HasChildrenProperty, value); }
}
// Using a DependencyProperty as the backing store for HasChildren. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HasChildrenProperty =
DependencyProperty.Register("HasChildren", typeof(bool), typeof(CustomPolygon),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
#endregion
private void polygon_MouseEnter(object sender, MouseEventArgs e)
{
if (IsSelected != true)
{
polygon.Stroke = new SolidColorBrush((Color)ColorConverter.ConvertFromString(Properties.Settings.Default.HotspotHover));
polygon.StrokeThickness = 1;
}
}
private void polygon_MouseLeave(object sender, MouseEventArgs e)
{
if (IsSelected != true)
{
polygon.Stroke = Brushes.Black;
polygon.StrokeThickness = 0.5;
}
}
private void polygon_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
IsSelected = !IsSelected;
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Problem is now if i change my selection in cmbHotspot the property is not changing in my usercontrol and visa versa
What they are telling in this blog is not correct for WPF. The article is not about WPF and what it shows is not even necessary in Windows Runtime. You should definitely ignore the parts about setting the DataContext in the constructor of a custom control.
Anyway, you must not call anything else than SetValue in the setter of the CLR wrapper of a dependency property. See the XAML Loading and Dependency Properties article on MSDN for details.
Besides that a dependency property does not need to raise a PropertyChanged event, so your SetValueIsSelected property is redundant anyway.
Finally, your property metadata was wrong, because 0 is not a valid default value for type bool.
Your declaration should look like this:
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register(
"IsSelected", typeof(bool), typeof(CustomPolygon));
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
Unless you don't want to set any non-standard property metadata, you don't need to specify the PropertyMetadata parameter of the Register method.
However, if you want that the property binds two-way by default, you have to set the appropriate flag by property metadata.
public static readonly DependencyProperty IsSelectedProperty =
DependencyProperty.Register(
"IsSelected", typeof(bool), typeof(CustomPolygon),
new FrameworkPropertyMetadata(
false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault);

Center a polygon within a Grid control

Can anyone tell me how to center a polygon object within a given row/column of a Grid control?
The example that I have tried is taken from msdn.
<Grid x:Name="LayoutRoot" >
<Polygon Points="300,200 400,125 400,275 300,200"
Stroke="Purple"
StrokeThickness="2"
HorizontalAlignment="Center"
VerticalAlignment="Center" >
<Polygon.Fill>
<SolidColorBrush Color="Blue" Opacity="0.4" />
</Polygon.Fill>
</Polygon>
Cheers,
Xam
Add the attributes :-
HorizontalAlignment="Center" VerticalAlignment="Center"
to the Polygon.
Although a height and width are implied by the bounds of the polygon, it defaults to the size of the container.
If you just set
HorizontalAlignment="Center" VerticalAlignment="Center"
it will position the polygon's top-left in the centre.
You also have to explicitly set the height and width of the polygon to centre it and retain its bounds
Sample polygon Xaml with dimensions added:
<Grid x:Name="LayoutRoot">
<Path Data="M0.5,41.5 L201,0.5 L302,115 L157.25,157 z" Fill="#FFF4F4F5" Stroke="Black" UseLayoutRounding="False" HorizontalAlignment="Center" VerticalAlignment="Center" Width="302.5" Height="157.5"/>
</Grid>
Maybe this answer applies here too.
It uses a CenterConverter
public class CenterConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values[0] == DependencyProperty.UnsetValue || values[1] == DependencyProperty.UnsetValue)
{
return DependencyProperty.UnsetValue;
}
double width = (double) values[0];
double height = (double)values[1];
return new Thickness(-width/2, -height/2, 0, 0);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And binds it in XAML like this
<Canvas>
<TextBlock x:Name="txt" Canvas.Left="40" Canvas.Top="40" TextAlignment="Center" Text="MMMMMM">
<TextBlock.Margin>
<MultiBinding Converter="{StaticResource centerConverter}">
<Binding ElementName="txt" Path="ActualWidth"/>
<Binding ElementName="txt" Path="ActualHeight"/>
</MultiBinding>
</TextBlock.Margin>
</TextBlock>
<Rectangle Canvas.Left="39" Canvas.Top="39" Width="2" Height="2" Fill="Red"/>
</Canvas>
To be able to use this in C# too and not only in XAML you need this class
public class Mover : DependencyObject
{
public static readonly DependencyProperty MoveToMiddleProperty =
DependencyProperty.RegisterAttached("MoveToMiddle", typeof (bool), typeof (Mover),
new PropertyMetadata(false, PropertyChangedCallback));
public static void SetMoveToMiddle(UIElement element, bool value)
{
element.SetValue(MoveToMiddleProperty, value);
}
public static bool GetMoveToMiddle(UIElement element)
{
return (bool) element.GetValue(MoveToMiddleProperty);
}
private static void PropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = sender as FrameworkElement;
if (element == null)
{
return;
}
if ((bool)e.NewValue)
{
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = new CenterConverter();
multiBinding.Bindings.Add(new Binding("ActualWidth") {Source = element});
multiBinding.Bindings.Add(new Binding("ActualHeight") {Source = element});
element.SetBinding(FrameworkElement.MarginProperty, multiBinding);
}
else
{
element.ClearValue(FrameworkElement.MarginProperty);
}
}
}
Use it in XAML like so
<Canvas>
<TextBlock Canvas.Left="40" Canvas.Top="40" TextAlignment="Center" Text="MMMMMM"
local:Mover.MoveToMiddle="True"/>
</Canvas>
Or in C# like so
Mover.SetMoveToMiddle(UIElement, true);
Alternatively you can manipulate the RenderTransform
An alternative would be to bind to RenderTransform instead of Margin. In this case, the converter would return
return new TranslateTransform(-width / 2, -height / 2);
and the attached property's callback method would contain these lines:
if ((bool)e.NewValue)
{
...
element.SetBinding(UIElement.RenderTransformProperty, multiBinding);
}
else
{
element.ClearValue(UIElement.RenderTransformProperty);
}
This alternative has the advantage that the effect of the attached property is visible in the Visual Studio designer (which is not the case when setting the Margin property).
In XAML this would look like this:
<Canvas>
<TextBlock x:Name="txt" Canvas.Left="40" Canvas.Top="40" TextAlignment="Center" Text="MMMMMM">
<TextBlock.RenderTransform>
<MultiBinding Converter="{StaticResource centerConverter}">
<Binding ElementName="txt" Path="ActualWidth"/>
<Binding ElementName="txt" Path="ActualHeight"/>
</MultiBinding>
</TextBlock.RenderTransform>
</TextBlock>
<Rectangle Canvas.Left="39" Canvas.Top="39" Width="2" Height="2" Fill="Red"/>
</Canvas>
TextBlock was the control in question of the original answer. This way should be applicable to all objects of the class UIElement though.
Note: all credit goes to the original poster of the above linked answer

Dependency property not working, trying to set through style setter

I am trying to set up a custom style for my newly made usercontrol, however i am getting the error : "Cannot convert the value in attribute 'Property' to object of type 'System.Windows.DependencyProperty'."
I thought i had set up Dependency properties but it seemed this was not the case, so i did some research and added:
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource", typeof(BitmapSource), typeof(Image));
to make this:
-- MyButton.Xaml.Cs --
namespace Client.Usercontrols
{
public partial class MyButton : UserControl
{
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource", typeof(BitmapSource), typeof(Image));
public MyButton()
{
InitializeComponent();
}
public event RoutedEventHandler Click;
void onButtonClick(object sender, RoutedEventArgs e)
{
if (this.Click != null)
this.Click(this, e);
}
BitmapSource _imageSource;
public BitmapSource ImageSource
{
get { return _imageSource; }
set
{
_imageSource = value;
tehImage.Source = _imageSource;
}
}
}
}
This unfortunately does not work. I also tried this:
public BitmapSource ImageSource
{
get { return (BitmapSource)GetValue(MyButton.ImageSourceProperty); }
set
{
SetValue(ImageSourceProperty, value);
}
}
But that did not work and the image was not shown and generated the same error as mentioned previously anyway.
Any ideas?
Regards Kohan.
-- MyButton.Xaml --
<UserControl x:Class="Client.Usercontrols.MyButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" MinHeight="30" MinWidth="40"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Button Width="Auto" HorizontalAlignment="Center" Click="onButtonClick">
<Border CornerRadius="5" BorderThickness="1" BorderBrush="Transparent" >
<Grid>
<Image Name="tehImage" Source="{Binding ImageSource}" />
<TextBlock Name="tehText" Text="{Binding Text}" Style="{DynamicResource ButtonText}" />
</Grid>
</Border>
</Button>
</UserControl>
-- MYButton Style --
<Style TargetType="{x:Type my:MyButton}" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type my:MyButton}">
<ContentPresenter />
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="ImageSource" Value="../Images/Disabled.png" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Biggest problem I see is that you're registering the property as owned by Image rather than by your UserControl. Change to:
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource", typeof(BitmapSource), typeof(MyButton));
If that doesn't work, will need to see your XAML.
The standard form for a dependency property is (i've added in your information):
public BitmapSource ImageSource
{
get { return (BitmapSource)GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value); }
}
/* Using a DependencyProperty as the backing store for ImageSource.
This enables animation, styling, binding, etc... */
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource",
typeof(BitmapSource),
typeof(MyButton),
new UIPropertyMetadata(null)
);
it seems like your also trying to pass through the dependency property to the ImageSource of the object called "tehImage". You can set this up to automatically update using the PropertyChangedCallback... this means that whenever the property is updated, this will call the update automatically.
thus the property code becomes:
public BitmapSource ImageSource
{
get { return (BitmapSource)GetValue(ImageSourceProperty); }
set { SetValue(ImageSourceProperty, value); }
}
/* Using a DependencyProperty as the backing store for ImageSource.
This enables animation, styling, binding, etc... */
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.Register("ImageSource",
typeof(BitmapSource), typeof(MyButton),
new UIPropertyMetadata(null,
ImageSource_PropertyChanged
)
);
private static void ImageSource_PropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
((MyButton)source).tehImage.ImageSource = (ImageSource)e.NewValue
}
Hopefully with the correctly registered dependency property, this will help you narrow down the issue (or even fix it)
Set the DataContext for your UserControl:
public MyButton()
{
InitializeComponent();
DataContext = this;
}
Alternatively, if you can't do that (since the DataContext is set to another object, for example), you can do this in your XAML:
<UserControl x:Class="Client.Usercontrols.MyButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" MinHeight="30" MinWidth="40"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
x:Name="MyControl">
<Button Width="Auto" HorizontalAlignment="Center" Click="onButtonClick">
<Border CornerRadius="5" BorderThickness="1" BorderBrush="Transparent" >
<Grid>
<Image Name="tehImage" Source="{Binding ElementName=MyControl, Path=ImageSource}" />
<TextBlock Name="tehText" Text="{Binding ElementName=MyControl, Path=Text}" Style="{DynamicResource ButtonText}" />
</Grid>
</Border>
</Button>
</UserControl>
The correct way of implementing a source for an Image in a user control in my opinion is not BitmapSouce. The easiest and best way (according to me again) is using Uri.
Change your dependency property to this (while also defining a change callback event):
ImageSourceProperty = DependencyProperty.Register(
"ImageSource", typeof (Uri), typeof (MyButton),
new FrameworkPropertyMetadata(new PropertyChangedCallback(OnImageSourceChanged)));
and the property to this:
public Uri ImageSource
{
get
{
return (Uri)GetValue(ImageSourceProperty);
}
set
{
SetValue(ImageSourceProperty, value);
}
}
Where your call back is like this:
private static void OnImageSourceChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
MyButton hsb = (MyButton)sender;
Image image = hsb.tehImage;
image.Source = new BitmapImage((Uri) e.NewValue);
}

WPF - Best Practice for the run-of-the-mill [Label:Input] Control

I am wondering, which is the best and quickest way to get the well known Label Input [or output, doesn't matter] combination in WPF. Its a simple Task, just think of a quick output of the "object" ME:
Name - Christian
Age - 28
Mood - Good
I know, I can use a Grid with TextBlocks. But to be honest, the "short" XAML for this is nearly half a page long (RowDefinitions, ColDefs, Grid.Col on each Label)
The alternative way, using three StackPanels (horizontal) with one vertical seems also a little bit stupid. In this case, I have to give each Label a fixed width, to get the indent correct. And it just does not "feel" right.
So, given the Situation above, you got a custom object with 3-6 Properties you just want to dump as readonly to your GUI, how would you do it (in WPF, Silverlight too, if you are really in the mood :).
I can, of course, write a usercontrol for this. But why reinvent the wheel, if it might be already there...
And finally, to illustrate even further, the example I just created in real life and was the reason for this post:
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Log Count" Width="100"/>
<TextBlock Text="{Binding LastLogRun.LogMessageCount}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Start Time" Width="100"/>
<TextBlock Text="{Binding LastLogRun.StartTime}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="End Time" Width="100"/>
<TextBlock Text="{Binding LastLogRun.EndTime}"/>
</StackPanel>
</StackPanel>
You could use shared size groups to get the auto-sizing Grid behavior of two nicely-lined-up columns, while still being able to pull out the complexity into a UserControl.
Here's an example of using a LabeledEdit control that would do what you're looking for. The complexity has all been factored away into the UserControl, and all you need to do is remember to set Grid.IsSharedSizeScope on the StackPanel:
<Window x:Class="WpfApplication5.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication5"
Name="Self" Title="Window1" Height="300" Width="300">
<StackPanel Grid.IsSharedSizeScope="True">
<local:LabeledEdit Label="Name"/>
<local:LabeledEdit Label="Age" Text="28"/>
<!-- and with databinding... -->
<local:LabeledEdit Label="Width"
Text="{Binding Width, ElementName=Self}"/>
<local:LabeledEdit Label="Height"
Text="{Binding Height, ElementName=Self}"/>
</StackPanel>
</Window>
And here's the source code for the UserControl. LabeledEdit.xaml:
<UserControl x:Class="WpfApplication5.LabeledEdit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Self">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabeledEdit_Labels"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Content="{Binding Label, ElementName=Self}"/>
<TextBox Grid.Column="1" Text="{Binding Text, ElementName=Self}"/>
</Grid>
</UserControl>
LabeledEdit.xaml.cs:
using System.Windows;
namespace WpfApplication5
{
public partial class LabeledEdit
{
public static readonly DependencyProperty LabelProperty =
DependencyProperty.Register("Label", typeof(object), typeof(LabeledEdit));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(LabeledEdit),
new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public LabeledEdit()
{
InitializeComponent();
}
public object Label
{
get { return GetValue(LabelProperty); }
set { SetValue(LabelProperty, value); }
}
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
}
}
If you're using 3.5sp1 you can use StringFormat in the binding. Something like this should work...
<TextBlock Text="{Binding LastLogRun.LogMessageCount, StringFormat={}Log Count - {0}}" />
Perhaps you should rethink your UI. Why would you want Label - Textbox on the same line? That's a horrendous waste of space.
Why not Label over texbox? Then you've got a simple UI and simple XAML:
<StackPanel Orientation="Vertical">
<TextBlock>Name</TextBlock>
<TextBox />
<TextBlock>Age</TextBlock>
<TextBox />
<TextBlock>Mood</TextBlock>
<TextBox />
</StackPanel>
Add some styling for your TextBlocks and you've got a nice, clean UI, with very little repetition.
The silverlight toolkit has a DataForm control that works pretty cool!
I know this is 13! years later, but, if anyone else is curious, you can use BulletDecorator (docs). There's special handling for vertical alignment based on the first line of text, if the content is text-based content.
OP's example, written with BulletDecorators:
<StackPanel>
<BulletDecorator>
<BulletDecorator.Bullet>
<TextBlock Text="Log Count" Width="100"/>
</BulletDecorator.Bullet>
<TextBlock Text="{Binding LastLogRun.LogMessageCount}"/>
</BulletDecorator>
<BulletDecorator>
<BulletDecorator.Bullet>
<TextBlock Text="Start Time" Width="100"/>
</BulletDecorator.Bullet>
<TextBlock Text="{Binding LastLogRun.StartTime}"/>
</BulletDecorator>
<BulletDecorator>
<BulletDecorator.Bullet>
<TextBlock Text="End Time" Width="100"/>
</BulletDecorator.Bullet>
<TextBlock Text="{Binding LastLogRun.EndTime}"/>
</BulletDecorator>
</StackPanel>
If you're not a fan of that 👆 syntax (it's a bit verbose for my sake), you can use some attached properties to clean it up some.
BulletHelper.cs
public static class BulletHelper
{
#region Bullet
public static readonly DependencyProperty BulletProperty = DependencyProperty.RegisterAttached(
"Bullet",
typeof(object),
typeof(BulletHelper),
new FrameworkPropertyMetadata(OnBulletChanged)
);
public static object? GetBullet(DependencyObject target)
=> target.GetValue(BulletHelper.BulletProperty);
public static void SetBullet(DependencyObject target, object? value)
=> target.SetValue(BulletHelper.BulletProperty, value);
private static void OnBulletChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BulletDecorator bulletDecorator)
{
bulletDecorator.Bullet = CreateUiElement(e.NewValue);
}
}
#endregion Bullet
#region Child
public static readonly DependencyProperty ChildProperty = DependencyProperty.RegisterAttached(
"Child",
typeof(object),
typeof(BulletHelper),
new FrameworkPropertyMetadata(OnChildChanged)
);
public static object? GetChild(DependencyObject target)
=> target.GetValue(BulletHelper.ChildProperty);
public static void SetChild(DependencyObject target, object? value)
=> target.SetValue(BulletHelper.ChildProperty, value);
private static void OnChildChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is BulletDecorator bulletDecorator)
{
bulletDecorator.Child = CreateUiElement(e.NewValue);
}
}
#endregion Child
[return: NotNullIfNotNull("value")]
private static UIElement? CreateUiElement(this object? value)
{
return value switch
{
null => null,
// Uncomment if using MaterialDesignThemes (https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit)
// PackIconKind kind => new PackIcon { Kind = kind },
// Uncomment if using FontAwesome6.Svg (https://github.com/MartinTopfstedt/FontAwesome6)
// EFontAwesomeIcon.None => null,
// EFontAwesomeIcon fontAwesomeIcon => new SvgAwesome { Icon = fontAwesomeIcon },
UIElement uiElement => uiElement,
_ => new ContentPresenter { Content = value },
};
}
}
The above attached properties reduces it to:
<StackPanel>
<BulletDecorator BulletHelper.Bullet="Log Count"
BulletHelper.Child="{Binding LastLogRun.LogMessageCount}" />
<BulletDecorator BulletHelper.Bullet="Start Time"
BulletHelper.Child="{Binding LastLogRun.StartTime}" />
<BulletDecorator BulletHelper.Bullet="End Time"
BulletHelper.Child="{Binding LastLogRun.EndTime}" />
</StackPanel>
Unfortunately, you lose out on the Width property that was previously set on the TextBlock.
I wish BulletDecorator was easier to work with. Unfortunately, its two main properties, Child and Bullet, are not dependency properties, and they both must be UIElement.
However. You can make a Control (not a UserControl!) that makes BulletDecorator easier to use.
BulletControl.cs
public class BulletControl : ContentControl
{
static BulletControl()
{
FocusableProperty.OverrideMetadata(typeof(Control), new FrameworkPropertyMetadata(false));
DefaultStyleKeyProperty.OverrideMetadata(typeof(BulletControl), new FrameworkPropertyMetadata(typeof(BulletControl)));
}
#region Bullet
public static readonly DependencyProperty BulletProperty = DependencyProperty.Register(
nameof(Bullet),
typeof(object),
typeof(BulletControl),
new FrameworkPropertyMetadata(BulletChangedCallback)
);
public object? Bullet
{
get => this.GetValue(BulletProperty);
set => this.SetValue(BulletProperty, value);
}
private static void BulletChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Bullet ctrl)
{
ctrl.SetValue(HasBulletPropertyKey, e.NewValue is not null);
}
}
#endregion Bullet
#region BulletStringFormat
public static readonly DependencyProperty BulletStringFormatProperty = DependencyProperty.Register(
nameof(BulletStringFormat),
typeof(string),
typeof(BulletControl),
new FrameworkPropertyMetadata()
);
public string? BulletStringFormat
{
get => (string?)this.GetValue(BulletStringFormatProperty);
set => this.SetValue(BulletStringFormatProperty, value);
}
#endregion BulletStringFormat
#region BulletTemplate
public static readonly DependencyProperty BulletTemplateProperty = DependencyProperty.Register(
nameof(BulletTemplate),
typeof(DataTemplate),
typeof(BulletControl),
new FrameworkPropertyMetadata()
);
public DataTemplate? BulletTemplate
{
get => (DataTemplate?)this.GetValue(BulletTemplateProperty);
set => this.SetValue(BulletTemplateProperty, value);
}
#endregion BulletTemplate
#region BulletTemplateSelector
public static readonly DependencyProperty BulletTemplateSelectorProperty = DependencyProperty.Register(
nameof(BulletTemplateSelector),
typeof(DataTemplateSelector),
typeof(BulletControl),
new FrameworkPropertyMetadata()
);
public DataTemplateSelector? BulletTemplateSelector
{
get => (DataTemplateSelector?)this.GetValue(BulletTemplateSelectorProperty);
set => this.SetValue(BulletTemplateSelectorProperty, value);
}
#endregion BulletTemplateSelector
#region HasBullet
private static readonly DependencyPropertyKey HasBulletPropertyKey = DependencyProperty.RegisterReadOnly(
name: nameof(HasBullet),
propertyType: typeof(bool),
ownerType: typeof(BulletControl),
typeMetadata: new FrameworkPropertyMetadata()
);
public bool HasBullet
{
get => (bool)this.GetValue(HasBulletPropertyKey.DependencyProperty);
private set => this.SetValue(HasBulletPropertyKey, value);
}
#endregion HasBullet
}
Generic.xaml
<Style TargetType="{x:Type controls:BulletControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:BulletControl}">
<BulletDecorator>
<BulletDecorator.Bullet>
<ContentPresenter ContentSource="Bullet" />
</BulletDecorator.Bullet>
<ContentPresenter ContentSource="Content" />
</BulletDecorator>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
With 👆, the OP's sample code becomes:
<StackPanel>
<StackPanel.Resources>
<DataTemplate x:Key="BulletTemplate">
<TextBlock Text="{Binding}"
Width="100" />
</DataTemplate>
</StackPanel.Resources>
<BulletControl Bullet="Log Count"
Content="{Binding LastLogRun.LogMessageCount}"
BulletTemplate="{StaticResource BulletTemplate}"
/>
<BulletControl Bullet="Start Time"
Content="{Binding LastLogRun.StartTime}"
BulletTemplate="{StaticResource BulletTemplate}"
/>
<BulletControl Bullet="End Time"
Content="{Binding LastLogRun.EndTime}"
BulletTemplate="{StaticResource BulletTemplate}"
/>
</StackPanel>

Resources