A few short explanations before final question.
I have to clone different UserControls and Panels in my WPF app. I decided to use next approach:
xamlString = XamlWriter.Save(control.Content);
stringReader = new StringReader(xamlString);
xmlReader = XmlReader.Create(stringReader);
restoredVisual = (Visual)XamlReader.Load(xmlReader);
Also I have next class:
public class UnitLabel : Label
{
public string Unit
{
get { return (string )GetValue(UnitProperty); }
set { SetValue(UnitProperty, value); }
}
public static readonly DependencyProperty UnitProperty = DependencyProperty.Register("Unit", typeof(string), typeof(UnitLabel), new PropertyMetadata(""));
}
And template for this class
<ControlTemplate x:Key="template" TargetType="local:UnitLabel">
<StackPanel Orientation="Horizontal">
<ContentPresenter />
<ContentPresenter Content="{TemplateBinding Unit}" />
</StackPanel>
</ControlTemplate>
Question: Unit string is not visible after deserialization but property Unit has correct value. Do you have any ideas how to fix the issue?
I have found the reason and understood that my question is really stupid. Controls lose thier style and templates after deserialization. I can set style and it solves my problem.
Controls lose their styles and templates after deserialization. I can set style and it solves my problem.
SUMMARY: After removing grouping have items in a data grid sort.
So I currently have a project with a user control that displays one of two controls depending on a datatype. Now when that control is a DataGrid, I have set up a style on the template that uses grouping.
<!--STYLE used to add grouping & expanders to data grid-->
<Style x:Key="GroupHeaderStyle" TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="False" >
<Expander.Header>
<TextBlock Text="{Binding Name}" TextDecorations="Underline"/>
</Expander.Header>
<ItemsPresenter/>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
And it's set up in the model like this so that the grouping is done on a property called "Group".
Data = new ListCollectionView(rows);
Data.GroupDescriptions.Add(new PropertyGroupDescription("Group"));
Now the user can toggle a toggle button to remove headings, and therefore just show a list of values.
public bool IsGrouped
{
get { return isGrouped; }
set
{
isGrouped = value;
OnPropertyChanged();
if (ConfigurationModel != null)
{
if (IsGrouped == true)
(ConfigurationModel as ConfigurationKeyPairModel).AddGrouping();
else
(ConfigurationModel as ConfigurationKeyPairModel).RemoveGrouping();
}
}
}
public void RemoveGrouping()
{
if(Data.GroupDescriptions.Count > 0)
Data.GroupDescriptions.RemoveAt(0);
}
public void AddGrouping()
{
if(Data.GroupDescriptions.Count < 1)
Data.GroupDescriptions.Add(new PropertyGroupDescription("Group"));
}
Once the grouping has been removed the item's go back to how you'd expect just a normal data grid. But they're still in the same positions they would have been in if they were grouped. I don't want to have the user manually click column headers(which are hidden anyway) I'd like to have this data sort itself straight away. I've had a look around for the past hour but haven't found any decent solutions that are MVVM or don't just have the user click column headers.
You could sort the CollectionView by a property by adding a SortDescription to its SortDescriptions property:
public void RemoveGrouping()
{
if (Data.GroupDescriptions.Count > 0)
Data.GroupDescriptions.RemoveAt(0);
Data.SortDescriptions.Clear();
Data.SortDescriptions.Add(new SortDescription("PropertyName", ListSortDirection.Ascending));
}
If there are no SortDescriptions added, the sort order is effectively undefined.
In C# UWP I am creating custom tooltip style.
I have changed the default style of tooltip as below.
<Style TargetType="ToolTip">
<Setter Property="Foreground" Value="White" />
<Setter Property="Background" Value="{ThemeResource SystemControlBackgroundChromeMediumLowBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource SystemControlForegroundChromeHighBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource ToolTipBorderThemeThickness}" />
<Setter Property="FontSize" Value="{ThemeResource ToolTipContentThemeFontSize}" />
<Setter Property="Padding" Value="40,40,40,35"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Grid Background="Transparent">
<Grid
MinWidth="100"
MinHeight="90"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Padding="15"
Background="Transparent">
<local:ArrowDown x:Name="arrowDown" TooltipPlacement="{TemplateBinding Placement}"/>
And my custom control ArrowDown is getting information of ToolTip placement, so I can show it depends if tooltip is under or above control.
In the ArrowDown control I have added a DependencyProperty as below:
public PlacementMode TooltipPlacement
{
get { return (PlacementMode)GetValue(TooltipPlacementProperty); }
set { SetValue(TooltipPlacementProperty, value); }
}
public static readonly DependencyProperty TooltipPlacementProperty =
DependencyProperty.Register("TooltipPlacement", typeof(PlacementMode), typeof(ArrowDown), new PropertyMetadata(null, TooltipPlacementChangedCallback));
private static void TooltipPlacementChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var self = (ArrowDown)d;
self.CalculateArrowVisibility();
}
// Method to show or hide arrow
private void CalculateArrowVisibility()
{
}
And the problem is that the CalculateArrowVisibility is fire only the first time when tooltip is shown, and it always returns Top for TooltipPlacement, no matter if tooltip is shown below or above control.
I need CalculateArrowVisibility to be fired whenever the tooltip is shown, and I need TooltipPlacement property to show if tooltip is Under or Above control.
Anyone have the idea about this?
The fact is that you cannot use the ToolTipService attached properties (e.g. <Button ToolTipService.Placement="Bottom" ToolTipService.ToolTip="!!!" />) to define the tooltip and it placement. This way the Placement is not set on the actual ToolTip control itself, and that's why it will always return Top.
In order to have the ToolTip pass down its Placement value to your custom dependency property, you will have to attach it like the following -
<Button>
<ToolTipService.ToolTip>
<ToolTip Placement="Bottom" Content="Hahaha..." />
</ToolTipService.ToolTip>
</Button>
Update
Turns out that even though the app Window pushes the tooltip above or below its parent, its Placement value is never changed, what's changed is its horizontal & vertical offsets.
So, in your case, if we could work out its exact vertical offset, we would be able to determine whether the tooltip is above or below (its parent).
Given we have a ToolTip Style in place, we can create an attached property of type ToolTip and attach it to the Grid that contains the ArrowDown control.
<Grid MinWidth="100"
MinHeight="90"
Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Padding="15"
Background="Transparent"
local:ToolTipHelper.ToolTip="{Binding RelativeSource={RelativeSource TemplatedParent}}">
Because the TemplatedParent of the Grid is the ToolTip, we can use RelativeSource binding to link the ToolTip on the screen with our attached property, as shown above.
Now, we have a reference to the actual ToolTip, let's find its offsets. After some digging, I've found that the offsets of the ToolTip are always 0, they are useless; however, the offsets of its parent - a Popup, sometimes gives me the correct values, but not always. This is because I was using the Opened event where those values weren't yet populated; as soon as I changed it to SizeChanged, they have been giving me the expected values.
public static class ToolTipHelper
{
public static ToolTip GetToolTip(DependencyObject obj)
{
return (ToolTip)obj.GetValue(ToolTipProperty);
}
public static void SetToolTip(DependencyObject obj, ToolTip value)
{
obj.SetValue(ToolTipProperty, value);
}
public static readonly DependencyProperty ToolTipProperty =
DependencyProperty.RegisterAttached("ToolTip", typeof(ToolTip), typeof(ToolTipHelper),
new PropertyMetadata(null, (s, e) =>
{
var panel = (Panel)s; // The Grid that contains the ArrowDown control.
var toolTip = (ToolTip)e.NewValue;
// We need to monitor SizeChanged instead of Opened 'cause the offsets
// are yet to be properly set in the latter event.
toolTip.SizeChanged += (sender, args) =>
{
var popup = (Popup)toolTip.Parent; // The Popup that contains the ToolTip.
// Note we have to use the Popup's offset here as the ToolTip's are always 0.
var arrowDown = (ArrowDown)panel.FindName("arrowDown");
arrowDown.TooltipPlacement = popup.VerticalOffset > 0
? PlacementMode.Bottom
: PlacementMode.Top;
};
}));
}
Now, with this approach, you should be able to use the ToolTipService attached properties too. So the following XAML would work.
<Button ToolTipService.ToolTip="!!!" Content="Hover Me" />
Hope this helps!
I am trying to convert a System.Windows.Shapes.Shape object into a System.Windows.Media.Geometry object.
With the Geometry object, I am going to render it multiple times with a custom graph control depending on a set of data points. This requires that each instance of the Geometry object has a unique TranslateTransform object.
Now, I am approaching the issue in two different ways, but neither seems to be working correctly. My custom control uses the following code in order to draw the geometry:
//Create an instance of the geometry the shape uses.
Geometry geo = DataPointShape.RenderedGeometry.Clone();
//Apply transformation.
TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y);
geo.Transform = translation;
//Create pen and draw geometry.
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
I have also tried the following alternate code:
//Create an instance of the geometry the shape uses.
Geometry geo = DataPointShape.RenderedGeometry;
//Apply transformation.
TranslateTransform translation = new TranslateTransform(dataPoint.X, dataPoint.Y);
dc.PushTransform(translation);
//Create pen and draw geometry.
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
dc.Pop(); //Undo translation.
The difference is that the second snippet doesn't clone or modify the Shape.RenderedGeometry property.
Oddly enough, I occasionally can view the geometry used for the data points in the WPF designer. However, the behavior is inconsistent and difficult to figure out how to make the geometry always appear. Also, when I execute my application, the data points never appear with the specified geometry.
EDIT: I have figured out how to generate the appearance of the geometry. But this only works in design-mode. Execute these steps:
Rebuild project.
Go to MainWindow.xaml and click in the custom shape object so that the shape's properties load into Visual Studio's property window. Wait until the property window renders what the shape looks like.
Modify the data points collection or properties to see the geometry rendered properly.
Here is what I want the control to ultimately look like for now:
How can I convert a Shape object to a Geometry object for rendering multiple times?
Your help is tremendously appreciated!
Let me give the full context of my problem, as well as all necessary code to understanding how my control is set up. Hopefully, this might indicate what problems exist in my method of converting the Shape object to a Geometry object.
MainWindow.xaml
<Window x:Class="CustomControls.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls">
<Grid>
<local:LineGraph>
<local:LineGraph.DataPointShape>
<Ellipse Width="10" Height="10" Fill="Red" Stroke="Black" StrokeThickness="1" />
</local:LineGraph.DataPointShape>
<local:LineGraph.DataPoints>
<local:DataPoint X="10" Y="10"/>
<local:DataPoint X="20" Y="20"/>
<local:DataPoint X="30" Y="30"/>
<local:DataPoint X="40" Y="40"/>
</local:LineGraph.DataPoints>
</local:LineGraph>
</Grid>
DataPoint.cs
This class just has two DependencyProperties (X & Y) and it gives a notification when any of those properties are changed. This notification is used to trigger a re-render via UIElement.InvalidateVisual().
public class DataPoint : DependencyObject, INotifyPropertyChanged
{
public static readonly DependencyProperty XProperty = DependencyProperty.Register("XProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));
public static readonly DependencyProperty YProperty = DependencyProperty.Register("YProperty", typeof(double), typeof(DataPoint), new FrameworkPropertyMetadata(0.0d, DataPoint_PropertyChanged));
private static void DataPoint_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
DataPoint dp = (DataPoint)sender;
dp.RaisePropertyChanged(e.Property.Name);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
public double X
{
get { return (double)GetValue(XProperty); }
set { SetValue(XProperty, (double)value); }
}
public double Y
{
get { return (double)GetValue(YProperty); }
set { SetValue(YProperty, (double)value); }
}
}
LineGraph.cs
This is the control. It contains the collection of data points and provides mechanisms for re-rendering the data points (useful for WPF designer). Of particular importance is the logic posted above which is inside of the UIElement.OnRender() method.
public class LineGraph : FrameworkElement
{
public static readonly DependencyProperty DataPointShapeProperty = DependencyProperty.Register("DataPointShapeProperty", typeof(Shape), typeof(LineGraph), new FrameworkPropertyMetadata(default(Shape), FrameworkPropertyMetadataOptions.AffectsRender, DataPointShapeChanged));
public static readonly DependencyProperty DataPointsProperty = DependencyProperty.Register("DataPointsProperty", typeof(ObservableCollection<DataPoint>), typeof(LineGraph), new FrameworkPropertyMetadata(default(ObservableCollection<DataPoint>), FrameworkPropertyMetadataOptions.AffectsRender, DataPointsChanged));
private static void DataPointShapeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
LineGraph g = (LineGraph)sender;
g.InvalidateVisual();
}
private static void DataPointsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{ //Collection referenced set or unset.
LineGraph g = (LineGraph)sender;
INotifyCollectionChanged oldValue = e.OldValue as INotifyCollectionChanged;
INotifyCollectionChanged newValue = e.NewValue as INotifyCollectionChanged;
if (oldValue != null)
oldValue.CollectionChanged -= g.DataPoints_CollectionChanged;
if (newValue != null)
newValue.CollectionChanged += g.DataPoints_CollectionChanged;
//Update the point visuals.
g.InvalidateVisual();
}
private void DataPoints_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ //Collection changed (added/removed from).
if (e.OldItems != null)
foreach (INotifyPropertyChanged n in e.OldItems)
{
n.PropertyChanged -= DataPoint_PropertyChanged;
}
if (e.NewItems != null)
foreach (INotifyPropertyChanged n in e.NewItems)
{
n.PropertyChanged += DataPoint_PropertyChanged;
}
InvalidateVisual();
}
private void DataPoint_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//Re-render the LineGraph when a DataPoint has a property that changes.
InvalidateVisual();
}
public Shape DataPointShape
{
get { return (Shape)GetValue(DataPointShapeProperty); }
set { SetValue(DataPointShapeProperty, (Shape)value); }
}
public ObservableCollection<DataPoint> DataPoints
{
get { return (ObservableCollection<DataPoint>)GetValue(DataPointsProperty); }
set { SetValue(DataPointsProperty, (ObservableCollection<DataPoint>)value); }
}
public LineGraph()
{ //Provide instance-specific value for data point collection instead of a shared static instance.
SetCurrentValue(DataPointsProperty, new ObservableCollection<DataPoint>());
}
protected override void OnRender(DrawingContext dc)
{
if (DataPointShape != null)
{
Pen shapePen = new Pen(DataPointShape.Stroke, DataPointShape.StrokeThickness);
foreach (DataPoint dp in DataPoints)
{
Geometry geo = DataPointShape.RenderedGeometry.Clone();
TranslateTransform translation = new TranslateTransform(dp.X, dp.Y);
geo.Transform = translation;
dc.DrawGeometry(DataPointShape.Fill, shapePen, geo);
}
}
}
}
EDIT 2:In response to this answer by Peter Duniho, I would like to provide the alternate method to lying to Visual Studio in creating a custom control. For creating the custom control execute these steps:
Create folder in root of project named Themes
Create resource dictionary in Themes folder named Generic.xaml
Create a style in the resource dictionary for the control.
Apply the style from the control's C# code.
Generic.xamlHere is an example of for the SimpleGraph described by Peter.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControls">
<Style TargetType="local:SimpleGraph" BasedOn="{StaticResource {x:Type ItemsControl}}">
<Style.Resources>
<EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5"/>
</Style.Resources>
<Style.Setters>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:DataPoint}">
<Path Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointFill}"
Stroke="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStroke}"
StrokeThickness="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointStrokeThickness}"
Data="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SimpleGraph}}, Path=DataPointGeometry}">
<Path.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Path.RenderTransform>
</Path>
</DataTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>
Lastly, apply the style like so in the SimpleGraph constructor:
public SimpleGraph()
{
DefaultStyleKey = typeof(SimpleGraph);
DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
}
I think that you are probably not approaching this in the best way. Based on the code you posted, it seems that you are trying to do manually things that WPF is reasonably good at handling automatically.
The main tricky part (at least for me…I'm hardly a WPF expert) is that you appear to want to use an actual Shape object as the template for your graph's data point graphics, and I'm not entirely sure of the best way to allow for that template to be replaced programmatically or declaratively without exposing the underlying transformation mechanic that controls the positioning on the graph.
So here's an example that ignores that particular aspect (I will comment on alternatives below), but which I believe otherwise serves your precise needs.
First, I create a custom ItemsControl class (in Visual Studio, I do this by lying and telling VS I want to add a UserControl, which gets me a XAML-based item in the project…I immediately replace "UserControl" with "ItemsControl" in both the .xaml and .xaml.cs files):
XAML:
<ItemsControl x:Class="TestSO28332278SimpleGraphControl.SimpleGraph"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl"
mc:Ignorable="d"
x:Name="root"
d:DesignHeight="300" d:DesignWidth="300">
<ItemsControl.Resources>
<EllipseGeometry x:Key="defaultGraphGeometry" Center="5,5" RadiusX="5" RadiusY="5" />
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:DataPoint}">
<Path Data="{Binding ElementName=root, Path=DataPointGeometry}"
Fill="Red" Stroke="Black" StrokeThickness="1">
<Path.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Path.RenderTransform>
</Path>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
C#:
public partial class SimpleGraph : ItemsControl
{
public Geometry DataPointGeometry
{
get { return (Geometry)GetValue(DataPointShapeProperty); }
set { SetValue(DataPointShapeProperty, value); }
}
public static DependencyProperty DataPointShapeProperty = DependencyProperty.Register(
"DataPointGeometry", typeof(Geometry), typeof(SimpleGraph));
public SimpleGraph()
{
InitializeComponent();
DataPointGeometry = (Geometry)FindResource("defaultGraphGeometry");
}
}
The key here is that I have an ItemsControl class with a default ItemTemplate that has a single Path object. That object's geometry is bound to the controls DataPointGeometry property, and its RenderTransform is bound to the data item's X and Y values as offsets for a translation transform.
A simple Canvas is used for the ItemsPanel, as I just need a place to draw things, without any other layout features. Finally, there is a resource defining a default geometry to use, in case the caller doesn't provide one.
And about that caller…
Here is a simple example of how one might use the above:
<Window x:Class="TestSO28332278SimpleGraphControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestSO28332278SimpleGraphControl"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<PathGeometry x:Key="dataPointGeometry"
Figures="M 0.5000,0.0000
L 0.6176,0.3382
0.9755,0.3455
0.6902,0.5618
0.7939,0.9045
0.5000,0.7000
0.2061,0.9045
0.3098,0.5618
0.0245,0.3455
0.3824,0.3382 Z">
<PathGeometry.Transform>
<ScaleTransform ScaleX="20" ScaleY="20" />
</PathGeometry.Transform>
</PathGeometry>
</Window.Resources>
<Grid>
<Border Margin="3" BorderBrush="Black" BorderThickness="1">
<local:SimpleGraph Width="450" Height="300" DataPointGeometry="{StaticResource dataPointGeometry}">
<local:SimpleGraph.Items>
<local:DataPoint X="10" Y="10" />
<local:DataPoint X="25" Y="25" />
<local:DataPoint X="40" Y="40" />
<local:DataPoint X="55" Y="55" />
</local:SimpleGraph.Items>
</local:SimpleGraph>
</Border>
</Grid>
</Window>
In the above, the only truly interesting thing is that I declare a PathGeometry resource, and then bind that resource to the control's DataPointGeometry property. This allows the program to provide a custom geometry for the graph.
WPF handles the rest through implicit data binding and templating. If the values of any of the DataPoint objects change, or the data collection itself is modified, the graph will be updated automatically.
Here's what it looks like:
I will note that the above example only allows you to specify the geometry. The other shape attributes are hard-coded in the data template. This seems slightly different from what you asked to do. But note that you have a few alternatives here that should address your need without requiring the reintroduction of all the extra manual-binding/updating code in your example:
Simply add other properties, bound to the template Path object in a fashion similar to the DataPointGeometry property. E.g. DataPointFill, DataPointStroke, etc.
Go ahead and allow the user to specify a Shape object, and then use the properties of that object to populate specific properties bound to the properties of the template object. This is mainly a convenience to the caller; if anything, it's a bit of added complication in the graph control itself.
Go whole-hog and allow the user to specify a Shape object, which you then convert to a template by using XamlWriter to create some XAML for the object, add the necessary Transform element to the XAML and wrap it in a DataTemplate declaration (e.g. by loading the XAML as an in-memory DOM to modify the XAML), and then using XamlReader to then load the XAML as a template which you can then assign to the ItemTemplate property.
Option #3 seems the most complicated to me. So complicated in fact that I did not bother to prototype an example using it…I did a little research and it seems to me that it should work, but I admit that I did not verify for myself that it does. But it would certainly be the gold standard in terms of absolute flexibility for the caller.
Overview
I have an application, that displays data from an observable collection. The observable collection is (in this debugging setting) created and instanciated only once, then the values stay the same.
The main view of the application contains a ListBox that is bound to said observable collection:
<ListBox x:Name="MainListBox" ItemsSource="{Binding Items}" SelectionChanged="MainListBox_SelectionChanged" >
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel MinWidth="456" MaxWidth="456" Background="White" Margin="0,0,0,17">
<sparklrControls:SparklrText Post="{Binding Path=.}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
<!-- Workaround used to stretch the child elements to the full width -> HorizontalContentAlignment won't work for some reason... -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The child items are bound to a UserControl. This UserControl implements a DependancyProperty which the child elements are bound to:
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(object), new PropertyMetadata(textPropertyChanged));
private static void postPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SparklrText control = d as SparklrText;
control.Post = (ItemViewModel)e.NewValue;
}
Binding to the post property configures other variables via the getter of the Post property
public ItemViewModel Post
{
get
{
return post;
}
set
{
if (post != value)
{
this.ImageLocation = value.ImageUrl;
this.Username = value.From;
this.Comments = value.CommentCount;
this.Likes = value.LikesCount;
this.Text = value.Message;
post = value;
}
}
}
This setter configures other which in turn set up elements in the user control. Nothing in the user control is bound, the few updates are done with direct access to the respective Content/Text properties. ImageLocation performs an asynchronous download of an image with
private void loadImage(string value)
{
WebClient wc = new WebClient();
wc.OpenReadCompleted += (sender, e) =>
{
image = new BitmapImage();
image.SetSource(e.Result);
MessageImage.Source = image;
};
wc.OpenReadAsync(new Uri(value));
}
Issue
When I scroll down in the list box and back up, the setter of Post is executed when the owning element comes back into view. The problem: value is a different instance of ItemViewModel. The ListBox ItemsSource is not accessed in any way from outside the class. When scrolling back up, it seems like the wrong Items are bound to the elements, resulting in distorted designs. Are there any issues with the Binding that cause this?
The issue was caused by the ListBox. Elements that are scroll out of view are recycled and appended on the other side. In the code above, a asynchronous operation did not check if the result was still valid, causing wrong display data.