Oct 29, 2006

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

34PolygonBinding3

TechEd Barcelona is coming up next week, between Tuesday the 7th and Friday the 10th. Some members of the Avalon team will be there, and I’m very fortunate to be part of that group. If you read my blog and are attending this conference, come by and introduce yourself (don’t be shy!!). I will spend most of my time helping with the Avalon labs, but you may also find me in the Avalon booth. I can’t wait to meet some of you, hear about the applications you’ve been developing with Avalon, brainstorm with you about your data binding scenarios, and hear all the feedback (good and bad) you have about this platform.

In my last post, I talked about a way to bind a Polygon’s Points to a data source that had the following advantages:

  • Changes in the source are propagated to the UI.
  • There is a clean separation between the UI and data layers.
  • It scales well for scenarios where you’re making small frequent changes to the source collection.

However, this solution had one drawback: it can not be used in Styles. Today I will show you a third solution to the same problem with all the advantages above, plus it can be used in Styles. I will be using the same data source I used in my previous post.

This time I decided to use a Converter. The code in the Convert method is very similar to the code in the ProvideValue method of the MarkupExtension from my previous post. In both implementations, we need to return the PointCollection that the Polygon’s Points property will be set to. Also, in both scenarios, we need to hook up an event handler to listen for collection changes in the source and replicate those changes in the PointCollection.

There are some differences, too. Of course, this time we don’t have to use reflection to get the source collection, since that gets passed as the first parameter of the Convert method. Another difference is that it is possible that one instance of the Converter will be used by several Bindings, which requires a little bit of coordination on our part. Here is the complete implementation of the Converter:

    public class PointCollectionConverter : IValueConverter
    {
        private Dictionary<IEnumerable<Point>, PointCollection> collectionAssociations;

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            IEnumerable<Point> enumerable = value as IEnumerable<Point>;
            if (enumerable == null)
            {
                throw new InvalidOperationException("Source collection must be of type IEnumerable<Point>");
            }

            // Construct a dictionary with source and target collection associations if that was not already done.
            if (this.collectionAssociations == null)
            {
                this.collectionAssociations = new Dictionary<IEnumerable<Point>, PointCollection>();
            }

            // If the source is already in the dictionary, return the existing PointCollection
            PointCollection points;
            if (this.collectionAssociations.TryGetValue(enumerable, out points))
            {
                return points;
            }
            else
            {
                // The source is not in the dictionary, so create a new point collection and add it to the dictionary.
                points = new PointCollection(enumerable);
                this.collectionAssociations.Add(enumerable, points);

                // Start listening to collection change events in the new source, if possible.
                INotifyCollectionChanged notifyCollectionChanged = enumerable as INotifyCollectionChanged;
                if (notifyCollectionChanged != null)
                {
                    notifyCollectionChanged.CollectionChanged += this.Source_CollectionChanged;
                }

                return points;
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException("ConvertBack should never be called");
        }

        private void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            IEnumerable<Point> enumerable = sender as IEnumerable<Point>;
            PointCollection points = this.collectionAssociations[enumerable];

            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    for (int i = 0; i < e.NewItems.Count; i++)
                    {
                        points.Insert(e.NewStartingIndex + i, (Point)e.NewItems[i]);
                    }
                    break;

                case NotifyCollectionChangedAction.Move:
                    for (int i = 0; i < e.NewItems.Count; i++)
                    {
                        points.RemoveAt(e.OldStartingIndex);
                        points.Insert(e.NewStartingIndex + i, (Point)e.NewItems[i]);
                    }
                    break;

                case NotifyCollectionChangedAction.Remove:
                    for (int i = 0; i < e.OldItems.Count; i++)
                    {
                        points.RemoveAt(e.OldStartingIndex);
                    }
                    break;

                case NotifyCollectionChangedAction.Replace:
                    for (int i = 0; i < e.NewItems.Count; i++)
                    {
                        points[e.NewStartingIndex + i] = (Point)e.NewItems[i];
                    }
                    break;

                case NotifyCollectionChangedAction.Reset:
                    points.Clear();
                    break;
            }
        }
    }

You probably noticed that the main difference between this code and the one in the MarkupExtension solution is the Dictionary. This is the coordination bit I talked about earlier. Let’s imagine for a while that, instead of this dictionary, we have two private members that hold the source collection and the PointCollection, just like in the MarkupExtension solution. Now imagine we have two Bindings that use this same Converter with different source collections. Here are the results of a common sequence of events:

  • The source collection of the second Binding changes. The private variables hold the source collection and PointCollection for the second Binding.
  • Now the source collection of the first Binding changes. Remember that the same event handler is used to handle changes to both source collections. The event handler is called, but makes the changes to the second PointCollection because that’s what the private variable holds.

As a general rule, holding state in a Converter is bad practice because it can cause trouble when shared.

I solved this problem by introducing a Dictionary that keeps an association between a source collection and the corresponding PointCollection. This way, the collection change handler is able to retrieve the PointCollection it needs to change at any point in time. Also, notice that if the same instance of the Converter is used twice with the same source collection, the second time it is used we return the PointCollection we already have.

Here is the XAML used in this solution:

    <Window.Resources>
        <local:PolygonItem x:Key="src"/>
        <local:PointCollectionConverter x:Key="converter"/>
    </Window.Resources>

    <StackPanel>
        <Button Click="ChangeSource" Margin="10" HorizontalAlignment="Center">Change data source</Button>
        <Polygon Name="polygonElement" Width="500" Height="500" Margin="25" Fill="#CD5C5C" Points="{Binding Source={StaticResource src}, Path=Points, Converter={StaticResource converter}}"/>
    </StackPanel>

The event handler for the Button is the same as in my previous post, so I won’t show it again.

Once again, the solution I explained sounds pretty good. So, what is the drawback this time? The one drawback I can think of is that we’re still holding state in the Converter - we are keeping an instance of the Dictionary. Sure, we are holding state so we don’t get in trouble by holding state some other way. In general, I would like to discourage people from keeping state in a Converter. In this case we put quite a bit of thought into the state we’re keeping around, so it’s not all that bad, but please use this technique with reservations.

I won’t bother showing a screenshot here, since the application for this post looks identical to the one in my previous post.

Here you can find the VS project with this sample code. This works with Beta2 WPF bits.


11 Comments
  1. Sheva

    Nice, Bea, but I think instead of using the biding source as the key for the dictionary, you can grab its hash code using GetHashCode() method, and use it as the key instead, it can be a bit more performant.

    Sheva

    • Bea

      Hi Sheva,

      That’s a very good point. Thanks a lot for your feedback.

      Bea

      • Anonymous

        Ummmm it seems using the hash code as the key would be very bad if you had a collision. You would either get an exception when you tried to add the duplicate key or you would end up overwriting the value with a completely different object just because the hashcodes happened to collide.

  2. Sheva

    Hi Bea, I just post my approach to polygon binding on my blog, you can take a glance at it here.

    Sheva

    • Bea

      Hi Sheva,

      I’m really glad to see my blog posts as the starting point for other blog posts. I like to see other people thinking about the same problems, and finding different approaches.

      Your solution is great, but I have two comments about it:

      - Your PointCollectionView intermediate object needs to know that the source data is of the type PolygonItem. A truly reusable solution should work for any source. This is of course not an issue if you always bind to the same source type in your application, and if you don’t intend to reuse this code in other scenarios.

      - I don’t see a reason why the Points property needs to be a DP. The source of a binding can be a simple CLR property.

      Thanks for thinking of this problem and for coming up with such a great solution.

      Bea

  3. Kent boogaart

    Hi Beatriz,

    I couldn’t find a suggestion box so I hope you don’t mind me making one here.

    I’d like to see how to use the BindingExpressionBase.UpdateTarget() method. I have bound a ListBox to a non-notifying collection.

    When I add an item to the collection the UI understandably does not update. I figured all I would have to do is get the binding expression (via BindingOperations) and call UpdateTarget. However, that does not do anything. The only way I have been able to get the UI to update is to set the DataContext to null and then back to the original object.

    Thanks,
    Kent

    • Bea

      Hi Kent,

      Regarding your comment on UpdateTarget, it is true that it doesn’t work in your scenario. When you call UpdateTarget, we check for the source object, and update the target only if the object is different. In your scenario, since the collection is still the same (we’re really comparing the pointer), we don’t do anything. So, basically, UpdateTarget will only work when the actual source object is replaced by some other different object.

      I agree with you that this is not very intuitive and that we should do something about it. I’ve added this to the list of discussions to have when planning for the next version.

      In the meantime, to achieve the behavior you want, you can simply add the following line instead of the UpdateTarget:

      lb.Items.Refresh();

      I’ve uploaded a simple application that does this here.

      Let me know if this fixes your problem.

      Bea

  4. Anonymous

    Hello Bea,

    Thanks for your post. I believe there are reasons you didn’t use ItemsControl for data binding in your project. Of course, complexity of code will be more if not the same. But I would appreciate if you can clarify.

    • Bea

      Hi,

      Regarding your comment about using ItemsControl… what solution exactly do you have in mind?

      I can think of one way to use ItemsControl: in each DataTemplate there would be a Line element with X2 and Y2 data bound to the current point and X1 and Y1 bound to RelativeSource=PreviousData. This would be terribly inneficient because instead of one element (Polyline) we would have at least two elements per line (the ContentControl generated by the ItemsControl for each item, plus the Line inside the DataTemplate).

      Unless you have in mind a more efficient way of using ItemsControl to solve this problem, I strongly discourage using it.

      Thanks,
      Bea

  5. Nat

    Hi,

    Nice article. Will it have memory leak problem? PointCollection seems to keep forever in synthetic singleton converter object.

    • Bea

      Hi Nat,

      Yes, it could have memory leaks - my code doesn’t ever remove anything from the dictionary. In this scenario, I’m not really sure how you would know when it is safe to remove an entry from the dictionary.

      Thanks for pointing that out,
      Bea

Comments are closed.