How to data bind a Polygon’s Points to a data source – Part II

In my last blog post, I talked about one solution to data bind a Polygon’s Points to a collection such that:
- Changes in the source are propagated to the UI.
- There is a clean separation between the UI and data layers.
This solution is good for scenarios where big changes to the collection happen each time we want to refresh the UI, but not so good when we need to frequently add or remove one point only. The reason for this is that every time we raise a collection change notification, a new PointCollection instance is created.
Today I will explain to you my thought process of coming up with a solution with the advantages of the previous one but without the drawback.
I will start by showing you the data source I use in this blog post:
public class PolygonItem
{
private double angleIncrement = Math.PI / Math.Sqrt(2);
private int initialCount = 249;
private ObservableCollection<Point> points = new ObservableCollection<Point>();
public ReadOnlyObservableCollection<Point> Points
{
get { return new ReadOnlyObservableCollection<Point>(this.points); }
}
public void AddPoint()
{
double angle = this.points.Count * this.angleIncrement;
double x = 250 + 250 * Math.Cos(angle);
double y = 250 + 250 * Math.Sin(angle);
this.points.Add(new Point(x, y));
}
public PolygonItem()
{
while (this.points.Count < this.initialCount)
{
this.AddPoint();
}
}
}
As you can see, every time the AddPoint method is called, a single point is added to the source collection (unlike the sample in the previous post, where every time you click on a button, the whole source collection is redone.) The fact that a very small change happens to the collection each time provides the motivation we need to find a solution different from the one in my last post.
In this post, I will explain how we can write a custom MarkupExtension that changes the target PointCollection when the source collection changes. My idea was that if Binding (which is a MarkupExtension too) doesn’t provide this functionality, I could write my own simple specialized Binding that does what I want.
To create the MarkupExtension, I started by thinking about which properties are needed to allow the user to specify the source collection. I got some inspiration from the Binding object and decided to pick two properties: “Source” and “PropertyName”. “Source” holds a source object, of any type, and “PropertyName” holds the name of a property of that object, of type IEnumerable<Point>. This is basically a very trimmed down version of Binding.
[MarkupExtensionReturnType(typeof(PointCollection))]
public class PointCollectionConnectorExtension : MarkupExtension
{
private object source;
private string propertyName;
private PointCollection pointCollection;
public object Source
{
get { return this.source; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
this.source = value;
}
}
public string PropertyName
{
get { return this.propertyName; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
this.propertyName = value;
}
}
(…)
}
Next I had to override the ProvideValue abstract method from MarkupExtension. This method is where you can find all the logic necessary to calculate the value provided by a MarkupExtension. In this particular scenario, I want to start listening to source collection change notifications and to modify the provided PointCollection everytime the source changes. Here is what my ProvideValue method looks like:
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (this.source == null || this.propertyName == null)
{
throw new InvalidOperationException("Cannot use PointCollectionConnector extension without setting Source and PropertyName.");
}
// Get the value of the property with name PropertyName from the source object.
Type sourceType = this.source.GetType();
PropertyInfo propertyInfo = sourceType.GetProperty(propertyName);
if (propertyInfo == null)
{
throw new InvalidOperationException(String.Format("Source object of type {0} does not have a property named {1}.", sourceType.Name, propertyName));
}
object propertyValue = propertyInfo.GetValue(this.source, null);
// See if the value is an enumerable collection of points.
IEnumerable<Point> enumerable = propertyValue as IEnumerable<Point>;
if (enumerable == null)
{
throw new InvalidOperationException(String.Format("Source object of type {0} has a property named {1}, but its value (of type {2}) doesn’t implement IEnumerable<Point>.", sourceType.Name, propertyName, propertyValue.GetType().Name));
}
// Construct the initial point collection by copying points from the enumerable collection.
this.pointCollection = new PointCollection(enumerable);
// Listen for collection changed events coming from the source, if possible.
INotifyCollectionChanged notifyCollectionChanged = propertyValue as INotifyCollectionChanged;
if (notifyCollectionChanged != null)
{
notifyCollectionChanged.CollectionChanged += this.Source_CollectionChanged;
}
return this.pointCollection;
}
As you can see, I use the Source and PropertyName properties to grab the source collection through reflection. The value provided by the MarkupExtension to the target DP is a new PointCollection created here, which contains the same points as the source collection. The most important part of this code, however, is where I hook up a listener for collection change notifications in the source collection. In that event handler, I have some code that replicates in the PointCollection any changes made to the source collection:
private void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
for (int i = 0; i < e.NewItems.Count; i++)
{
this.pointCollection.Insert(e.NewStartingIndex + i, (Point)e.NewItems[i]);
}
break;
case NotifyCollectionChangedAction.Move:
for (int i = 0; i < e.NewItems.Count; i++)
{
this.pointCollection.RemoveAt(e.OldStartingIndex);
this.pointCollection.Insert(e.NewStartingIndex + i, (Point)e.NewItems[i]);
}
break;
case NotifyCollectionChangedAction.Remove:
for (int i = 0; i < e.OldItems.Count; i++)
{
this.pointCollection.RemoveAt(e.OldStartingIndex);
}
break;
case NotifyCollectionChangedAction.Replace:
for (int i = 0; i < e.NewItems.Count; i++)
{
this.pointCollection[e.NewStartingIndex + i] = (Point)e.NewItems[i];
}
break;
case NotifyCollectionChangedAction.Reset:
this.pointCollection.Clear();
break;
}
}
Here is the XAML that uses this MarkupExtension:
<Window.Resources>
<local:PolygonItem x:Key="src"/>
</Window.Resources>
<Polygon Name="polygonElement" Width="500" Height="500" Margin="10" Fill="#CD5C5C">
<Polygon.Points>
<local:PointCollectionConnector Source="{StaticResource src}" PropertyName="Points"/>
</Polygon.Points>
</Polygon>
I am using property-element syntax to add the MarkupExtension in this case. Unfortunately there is a bug that prevents me from using attribute syntax with a custom MarkupExtension:
<Polygon Points="{PointCollectionConnector Source={StaticResource src}, PropertyName=Points}" />
Next I supplied a Button with an event handler that adds a point to the source collection each time the Button is pressed.
<Button Click="ChangeSource" Margin="10" HorizontalAlignment="Center">Change data source</Button>
private void ChangeSource(object sender, RoutedEventArgs e)
{
PolygonItem polygonItem = this.Resources["src"] as PolygonItem;
polygonItem.AddPoint();
// Unfortunately, the Polygon element won’t update unless we call InvalidateMeasure and InvalidateVisual.
this.polygonElement.InvalidateMeasure();
this.polygonElement.InvalidateVisual();
}
Unfortunately there is a bug here too: the UI will not reflect changes in the PointCollection. To work around it, we need to call InvalidateMeasure followed by InvalidateVisual, as shown above.
If you now click on the Button, you will see that a new Point is added to the collection, and the pattern in the UI changes. This solution sounds pretty good, but it has a drawback: you can not use this MarkupExtension in a Style because the PointCollection, which is a Freezable, freezes (meaning, it can not be changed). Freezables become frozen when used in Styles because it’s the only way they can be used across threads. It’s very common for Styles (particularly those in the theme files) to be used in multiple threads.
I like this solution, but of course I couldn’t stop thinking about this until I was able to find a solution with all the advantages of this one, and without the drawback. I will talk about a third solution in my next blog post, so stay tuned.
Here is a screenshot of this application:
Here you can find the VS project with this sample code. This works with Beta2 WPF bits.
Sheva
Hi, Bea, nice article, actually you can use property-attribute syntax to specifiy the markup extension as long as your custom markup extension is pre-compiled, which means that your custom markup extension should be put into another assembly. and another thing which I cannot understand is that the PointCollectionProperty has FrameworkPropertyMetaDataOptions.AffectsRender and FrameworkPropertyMetaDataOptions.AffectsMeasure marked, then why should we manually call the InvalidateMeasure and InvalidateVisual explicitly.
Sheva
October 18, 2006 at 7:16 am
Bea
Hi Sheva,
Ah, I haven’t tried pre-compiling the MarkupExtension but I believe you, and thanks for the tip.
I opened the bug about the Invalidate calls, but that hasn’t been looked at, so I don’t know the technical reasons why it’s behaving this way. I am hoping it’s a simple bug that we can fix for next version. I agree with you we shouldn’t have to call those manually.
Thanks for your comments.
Bea
October 19, 2006 at 9:51 am
captain ramen
I have a question about binding ItemsControls. Let’s say I had
public class Address
{
public string CountryName
{ get; set; }
public string Country
{ get; set; }
}
Binding the two properties to a TextBox is easy enough, but what about a ComboBox, where the ComboBox is bound to a list of possible values (in this case a Dictionary`1[String, String]). I’ve looked everywhere but I can’t figure out how to propogate the SelectedValue back to the Address without going into codebehind. Is it even possible to do this declaratively?
October 20, 2006 at 4:21 pm
Bea
Hi,
I have a few questions about what you’re trying to do. What do you mean by “propagate the SelectedValue back to the Address”? What exactly do you want to happen when you select a value? What does your data structure look like, is it a Dictionary of [CountryName, Country] and an instance of Address that holds the SelectedValue?
I should be able to help you once I understand exactly what your app looks like.
Thanks,
Bea
October 21, 2006 at 7:02 pm
Vishal G Pillai
Hi Beatriz,
I have a doubt about getting an element from a DataTemplate. I will explain the scenario.
I have created a user control ( let say it as X )which exposes property which of of type DataTemplate. Another user control( let say it as Y ) uses X. to create another usable user control Z. But while we are applying template to X from Y, I need some element in the DataTemplate to do some custom operation. But I am not able to get that. I am providing the code here to check.
Here is the template
??DataTemplate x:Key=”dingTemplate”??
??StackPanel Orientation=”Horizontal” Margin=”2″ Name=”stak” ??
??Grid Width=”{Binding RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}, Path=ActualWidth}”??
??Grid.ColumnDefinitions??
??ColumnDefinition Width=”Auto”/??
??ColumnDefinition Width=”*”/??
??ColumnDefinition Width=”Auto”/??
??/Grid.ColumnDefinitions??
??TextBlock Text=”xxxx :” Grid.Column=”0″ Padding=”4,2,4,2″ VerticalAlignment=”Center”/??
??TextBox Name=”yyy” Grid.Column=”1″ Margin=”4,0,4,0″ /??
??Button Name=”buttonTemp” Content=”ZZZ” Grid.Column=”2″ Padding=”12,2,12,2″/??
??/Grid??
??/StackPanel??
??/DataTemplate??
And it is applied to another user control like
??DingDongControl x:Name=”dingdongCtrl” CustomHeaderTemplate=”{StaticResource dingTemplate}” ??
I need the element Button ( “buttonTemp”) from that template. How I can get that
Vishal
September 6, 2007 at 1:48 pm
Bea
Hi Vishal,
I showed in this blog post how you can get a handle to an element within a DataTemplate. You need to call the FindName method on the instance of the DataTemplate, and pass the name of the element you’re interested in, and an instance of the object the template is being applied to (in your case, the UserControl).
Thanks,
Bea
September 19, 2007 at 11:47 pm