Creating fully binded graph control with WPF

Today we’ll try to understand how powerfull is data binding engine in WPF. We’ll create graph control, that works without loops, overriding OnPaint events and other dirty tricks we were used in GDI+ world.

So WPF Polyline element looks like useful for us to create some kind of graph. So we’ll add this like in our code.

 
<Polyline Name="myGraph"
                Width="Auto"
                Height="Auto"
                Fill="Green"
                >
</Polyline>
 

Now, we have something that might be used for drawing lines of our graph. So, now we can start adding points to its Points collection and create graph. But stop. Why? Let’s see how can we bind data directly to this property and let WPF rendering engine to work for us.

In order to do it, we’ll need some points collection, that knows to tell everyone about it’s update. So we have interesting interface INotifyPropertyChanged, that can notify subscribers about any changes happens.

class PointItem:INotifyPropertyChanged

    {
 
        #region INotifyPropertyChanged Members
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        void _points_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs("Points"));
            }
        }
        #endregion
 
        #region Properties
        ObservableCollection<Point> _points = new ObservableCollection<Point>();
        public ReadOnlyObservableCollection<Point> Points
        {
            get { return new ReadOnlyObservableCollection<Point>(_points); }
        }
 
        bool _isFinished = true;
        public bool IsFinished
        {
            get { return _isFinished; }
            private set 
            {
                _isFinished = value;
                PropertyChanged(this, new PropertyChangedEventArgs("IsFinished"));
            }
        }
        #endregion
 
 
        int _maxPoints = 100;
        double _tFactor;
 
        #region ctor
        public PointItem()
        {
            //Notify us about the internal collection changed
            _points.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_points_CollectionChanged);
 
            Init();
        }
        #endregion
 
        #region Methods
        public void Init()
        { 
            //let's init all we have
 
            //Here we begin. What we need is range or -PI to PI.
            _tFactor = -Math.PI;
 
            //clean up all point;
            _points.Clear();
 
            //reset isFinished property, we are in the very begining. We do not want to rise property change event in this case
            _isFinished = false;
        }
        public void GeneratePoint(double maxWidth, double maxHeight)
        {
            if (_tFactor >= Math.PI)
            {
                //we finished, just return
                IsFinished = true;
                return;
            }
 
            //don't bother yourself with math. It's not really important
            double R = (1 + Math.Sin(_tFactor)) * (1 + 0.9 * Math.Cos(8 * _tFactor)) * (1 + 0.1 * Math.Cos(24 * _tFactor)) * maxWidth / 10;
            Point p = new Point(
                maxWidth / 2 + (R * Math.Cos(_tFactor)),
                maxHeight / 2 - (R * Math.Sin(_tFactor)));
 
            //Now just add point
 
            AddPoint(p);
            _tFactor += Math.PI / _maxPoints;
 
        }        
        public void AddPoint(Point point)
        {
            _points.Add(point);
        }
        #endregion
 
    }

 

Very good. Now the only thing we have to do is set this collection as data source for our polyline. But how to do it? In order to performs such task we’ll have to bring the instance of our PointItem class into XAML

<Page.Resources>

    <my:PointItem x:Key="mySource"/>
</Page.Resources>

Very good. So, how the last thing to do is bind it.

 

<Polyline Name="myGraph"
                Width="Auto"
                Height="Auto"
                Fill="Green"
                Points="{Binding Source={StaticResource mySource}, Path=Points}">
</Polyline>

Let’s compile it and see what happend. Nothing? Really strange… Wait. What data type we have in Points property? Oh, that’s ReadOnlyObservableCollection, but Polyline wants to recieve PointCollection as data source… What to do? We have to convert it somehow.

One of Binding’s properties we have Converter. What is it? Can it be helpfull for us? While reading MSDN documentation we’ll notice, that Converter must implement IValueConverter interface. It has two metods Convert and ConvertBack. That’s exactly what we need. Let’s write converter

 

class DataPointConverter : IValueConverter
    {
        private Dictionary<IEnumerable<Point>, PointCollection> _collectionAssoc;
 
        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>");
            }
 
            // Now we'll construct source dictionary if this was not done
            if (this._collectionAssoc == null)
            {
                this._collectionAssoc = new Dictionary<IEnumerable<Point>, PointCollection>();
            }
 
            // Get our point collection back, if the source is already in the dictionary
            PointCollection _points;
            if (this._collectionAssoc.TryGetValue(_enumerable, out _points))
            {
                return _points;
            }
            else
            {
                // Ops, the source is not in the dictionary, so let's create a new point collection and add it to our dictionary.
                _points = new PointCollection(_enumerable);
                this._collectionAssoc.Add(_enumerable, _points);
 
                // We have to listen to changes of the collection and fire PointCollection event each time we got List changes
                // If we can listen to the collection changes, let's do it
                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("Why? Why you want to convert it back. There is nothing, that can support it");
        }
 
        private void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            IEnumerable<Point> enumerable = sender as IEnumerable<Point>;
            PointCollection points = this._collectionAssoc[enumerable];
            if (enumerable != null && points != null)
            {
                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;
                }
            }
            else 
            {
                throw new InvalidCastException("Something gone wrong. It can not be something else then IEnumerable<Point> as sender fot this method");
            }
        }
    }

Great. Well done, now we have to bring it into our XAML

 

<Page.Resources>
  <my:PointItem x:Key="mySource"/>
  <my:DataPointConverter x:Key="myPointsConverter"/>
</Page.Resources>

And connect it to our Points property

 

<Polyline Name="myGraph"
                Width="Auto"
                Height="Auto"
                Fill="Green"
                Points="{Binding Source={StaticResource mySource}, Path=Points, Converter={StaticResource myPointsConverter}}">
</Polyline>

Now we can compile it and see the result. Great! That’s work! Lets’s see what we have in CPU?

Oh, my godness! Almost nothing. Let’s add some effects to make it looks better.

That’s it. Now we have ready to use vector graph page, that can get points and build graph instead of us. Thank you, WPF.

Application source code

  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • DotNetKicks
  • DZone
  • Live
  • Reddit
  • TwitThis
  • email
  • Slashdot
  • StumbleUpon

You may also be interested with:

  1. Real singleton approach in WPF application
  2. INotifyPropertyChanged auto wiring or how to get rid of redundant code

⟨ , ,  ⟩

No comments yet

Leave a Reply

Recommended

 


Sponsor


Partners

WPF Disciples
Dreamhost
Code Project
Switched to Better Place

Together