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.
Related
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);
My OnPropertyChangedCallback is not being called for an attached property.
I expect this method to be called by default when running the app.
My code compiles.
I have no errors in the Output window.
Any suggestions?
Attached Property:
namespace FileModifier.Behaviors
{
public class AttachedProperties : DependencyObject
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(AttachedProperties), new PropertyMetadata(null, OnPropertyChangedCallback));
public static void SetText(DependencyObject d, string value)
{
d.SetValue(TextProperty, value);
}
public static string GetText(DependencyObject d)
{
return d.GetValue(TextProperty) as string;
}
private static void OnPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var richEditBox = d as RichTextBox;
var textRange = new TextRange(richEditBox.Document.ContentStart, richEditBox.Document.ContentEnd);
throw new NotImplementedException();
}
}
}
XAML:
<RichTextBox x:Name="FileContentControl" IsReadOnly="True"
behaviors:AttachedProperties.Text="{Binding ElementName=FileListView, Path=SelectedItem,
Converter={StaticResource FilePathToContentConverter}}"
VerticalAlignment="Center" Padding="10" />
.
.
.
<ListView x:Name="FileListView" ItemsSource="{Binding FilePaths}" Grid.Column="2" Grid.Row="0"
SelectedItem="{Binding SelectedFilePath}"
VerticalAlignment="Center"
Margin="5">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource TextTruncationConverter}}" />
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="Blue" />
<Setter Property="Padding" Value="5" />
</Style>
</ListView.ItemContainerStyle>
</ListView>
You do not register attached property but normal DependencyProperty. You should use DependencyProperty.RegisterAttached instead. Also if your class contains only attached properties it may as well be static class. It does not have to inherit from DependencyObject
public static class AttachedProperties
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.RegisterAttached("Text", typeof(string), typeof(AttachedProperties), new PropertyMetadata(null, OnPropertyChangedCallback));
public static void SetText(DependencyObject d, string value)
{
d.SetValue(TextProperty, value);
}
public static string GetText(DependencyObject d)
{
return d.GetValue(TextProperty) as string;
}
private static void OnPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var richEditBox = d as RichTextBox;
var textRange = new TextRange(richEditBox.Document.ContentStart, richEditBox.Document.ContentEnd);
throw new NotImplementedException();
}
}
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
I'm working on a dynamicaly created grid containing in each new column a new item added to an ItemsControl.
I'm using Rachel Lim's GridHelper
For now, i have a main window like following,
Xaml code:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:StarsConverter x:Key="conv"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="9*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Command="{Binding Add}"/>
<ItemsControl x:Name="list" Grid.Column="1" ItemsSource="{Binding Oc}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid x:Name="grid" Background="Black"
local:GridHelpers.StarColumns="{Binding ColumnCount, Converter={StaticResource conv}, Mode=OneWay}" local:GridHelpers.ColumnCount="{Binding ColumnCount}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas Loaded="canvas_Loaded" x:Name="canvas" Background="White">
<Border Canvas.Left="25" Canvas.Top="25" Height="25" Width="50" Background="Red">
<TextBlock Text="{Binding}"/>
</Border>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
and Xaml.cs
public partial class MainWindow : Window
{
private Canvas canvas;
public MainWindow()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
private void canvas_Loaded(object sender, RoutedEventArgs e)
{
ViewModel vm = this.DataContext as ViewModel;
canvas = sender as Canvas;
Binding b = new Binding();
b.Source = vm;
b.Path = new PropertyPath(ViewModel.ColumnCountProperty);
b.Mode = BindingMode.OneWay;
canvas.SetBinding(Grid.ColumnProperty, b);
}
}
This MainWindow has a ViewModel as DataContext:
public class ViewModel : DependencyObject
{
private RelayCommand add;
public ViewModel()
{
}
public ObservableCollection<String> Oc
{
get { return (ObservableCollection<String>)GetValue(OcProperty); }
set { SetValue(OcProperty, value); }
}
// Using a DependencyProperty as the backing store for Oc. This enables animation, styling, binding, etc...
public static readonly DependencyProperty OcProperty =
DependencyProperty.Register("Oc", typeof(ObservableCollection<String>), typeof(ViewModel), new UIPropertyMetadata(new ObservableCollection<string>()));
public int ColumnCount
{
get { return (int)GetValue(ColumnCountProperty); }
set { SetValue(ColumnCountProperty, value); }
}
// Using a DependencyProperty as the backing store for ColumnCount. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnCountProperty =
DependencyProperty.Register("ColumnCount", typeof(int), typeof(ViewModel), new UIPropertyMetadata(0));
public RelayCommand Add
{
get
{
if (this.add == null)
this.add = new RelayCommand(param => this.AddString(), param => this.CanAddString());
return this.add;
}
}
private bool CanAddString()
{
return true;
}
private void AddString()
{
this.Oc.Add("test" + ColumnCount);
ColumnCount++;
}
}
When i click the button, the command is doing fine, so i have a new itemscontrol item and my grid as ItemsPanelTemplate is updating with new ColumnDefinition but the item is not in the right column.
I know its a late answer :)
A more elegant solution would be adding two Dependency Properties Row and Column to your Items ViewModel
then in your ItemsControl you define an ItemContainerStyle:
<ItemsControl>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Grid.Row" Value="{Binding Path=Row}" />
<Setter Property="Grid.Column" Value="{Binding Path=Column}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
when you populate your ItemsSource collection you have set The Row and Column properties:
for (uint i = 0; i < yourCollection.Count; ++i )
{
var item = yourCollection[i];
item.Row = (int)(i / ColumnCount);
item.Column = (int)(i % ColumnCount);
}
In fact the canvas_loaded thing is useless...
The problem was coming from the GridHelpers class in the ColumnCountChanged.
I added this at the end of the ColumnCountChanged event after SetStarColumns(grid) in order to add the items in the corresponding cells:
for (int i = 0; i < childCount; i++)
{
var child = grid.Children[i] as FrameworkElement;
Grid.SetColumn(child, i);
}
and it worked!
Edit: I also used a converter (called "conv" in the Xaml sample above) for the OneWay binding of the GridHelper.StarColumns Property on a ColumnCount ViewModel Property to have all the newly created columns to be equally sized:
public class StarsConverter : IValueConverter
{
public object Convert(object value, Type TargetType, object parameter, CultureInfo culture)
{
int i = (int)value;
string s = "0";
for (int j = 1; j < i; j++)
{
s = s + ',' + j;
}
return s;
}
public object ConvertBack(object value, Type TargetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
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);
}