Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

Real Time StockFeed DashBoard (Applying Observer Design Pattern using WPF and C#)

4.80/5 (9 votes)
27 Dec 2013CPOL12 min read 39.4K   2.7K  
This article shows the application of Observer Design pattern to real life scenario.

Observer Demo Screenshot

Background

In software development we commonly come across scenarios which we might have implemented during different projects.Design patterns are solutions to such commonly recurring problems and designers of Object Orient Programming have provided us various design patterns which enables us to solve specific problem in best possible way. In this article I will be showing you the application of one of the most popular design pattern Observer and its application to real life scneario. For more information on design patterns please refer this link.

Observer is a design pattern which is also some times referred as Publisher/Subscriber pattern and is useful when various stakeholders may show interest in particular subject. They can subscribe to it. The subject when it changes its state then it will notify its subscribers and they will update themeslevels.The scenario taken for the application used in this article is Real Time StockFeed Dashboard which is of particular interest to stock brokers and investors who may be interested in monitoring in real time trend of key stocks of interest to them.

Introduction

Purpose of this article is to demonstrate the application of Observer Design pattern to real life scenario. In this article I have taken the example of Real Time Stock Feed Dashboard which shows real time trend of four key stocks. I have taken three types of observers in this example who are interested in change in the StockFeed and update or use them in different fashion. Below list shows the three categories of observers used in this example

  • Real Time Trend Control ( 4 Views)
  • Rolling Display of Stock Feeds in Application Status Bar.
  • StockFeedXML Exporter.This exports the stock feed in XML format.

This article will provide detailed implementation of this pattern using WPF and C#. In this article I will also explore following features of WPF

  • Use of Different Layout Panels
  • Data Binding
  • Themes in WPF
  • Graphics Capabilities in WPF
  • Toolbar and use of command design pattern
  • Lambda Expressions

Design Overview

Figure 1 shows the class diagram of the Real Time Stock Dashboard Application.Since most of the observers are updating the GUI Elements which is not allowed to be updated from different thread and hence the change is indirectly notified to all observers. This is done during Dispatcher.Invoke method and I will cover the role of this method in detail in later section.

Class Diagram of Custom Pane and Ribbon

Figure 1: Basic Class Diagram of Demo Application

From the class diagram IObserver is an interface which is implemented by all the observers TrendGraphControl, StatusBoard and StockFeedXMLExporter. StockFeedObservable is a class which is responsible for publishing stockPriceChanged event and all interested observers will subscribe to this event. FeedGenerator is a class which is responsible for simulating the feed and provides the new StockFeed value. StockFeedObservable class runs a thread in background to receive the new StockFeed value from FeedGenerator class by invoking it's GetNextFeed() method. StockFeedObservable class raises the event whenever it receives the new feed and notifies all it's subscribers. StockFeedEventArgs is a class which wraps the StockFeed. All the concrete observers then update themselves when they recieve notification.

Business Layer 

StockFeed is a main model of this application. All the observers are interested in this subject. I will explain detailed implementation of FeedGenerator class which acts as a data source for providing a new feed.

Implemention of FeedGenerator 

FeedGenerator is a class which is responsible to provide stock feed for different stocks. For the purpose of this application I have simulated the data for the feed for the key stocks with some fictitious data and source data for these key stocks is maintained in CSV files. FeedGenerator class then read these files and maintains them as a collection of StockFeed entities. Since it is for four different stocks each of the collection is maintained in a dictionary. The GetNextFeed() method then returns new value of StockFeed from this collection. The implementation details are given below.

  public class FeedGenerator
    {

        private string _filePath = string.Empty;
        private Dictionary<string, List<StockFeed>> _stockFeed= new Dictionary<string,List<StockFeed>>();
        private Dictionary<string, int> _stockIndexList = new Dictionary<string,int>();
        private const string STOCK_FEED_PATH = @"\ObserverDemo\Stocks\";

      

        /// <summary>
        /// Method:InitFeed
        /// Purpose:Initialises the Feed data
        /// </summary>
        public void InitFeed(string [] files) {

            try
            {
            _filePath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
    
            foreach(string f in files){
                _stockIndexList.Add(f, 0);
                string path = _filePath+STOCK_FEED_PATH+f+".csv";
               Logger.Log.Information(string.Format("Reading Stock Feed File: {0}", path));
                int index = 0;
                List<StockFeed> stockFeedList= new List<StockFeed>();

               using (StreamReader sr = new StreamReader(path))
               {
                   while (!sr.EndOfStream)
                   {
                      
                       string line = sr.ReadLine();
                       StockFeed sf=null;
                       if(index >0){
                         sf = ExtractStockFeedFromLine(line);
                            stockFeedList.Add(sf);
                       }
                       index++;
                   
                   }
                  
                   
               }
               _stockFeed.Add(f,stockFeedList);
               Logger.Log.Information(string.Format("No of records processed from file {0} are {1}",f, stockFeedList.Count));
            }
            }catch(Exception ex){
                string message ="Error occured while initialising the stock feed and error is " +ex.ToString();
                Logger.Log.Error(message);
                
            }
        }

        /// <summary>
        /// Method:ExtractFeedFromLine
        /// Purpose:Extracts the feed data from line.
        /// </summary>
        /// <param name="line"></param>
        /// <returns></returns>
        private StockFeed ExtractStockFeedFromLine(string line)
        {
            string[] aryValues = line.Replace("\"","").Split(',');
            StockFeed sf = new StockFeed();
            sf.StockCode = aryValues[0];
            sf.StockFeedDate = DateTime.Now;
            sf.MaxPrice = Decimal.Parse(aryValues[5]);
            sf.MinPrice = Decimal.Parse(aryValues[6]);
            sf.Price = Decimal.Parse(aryValues[8]);
            return sf;

        }

        /// <summary>
        /// Method:GetNextFeed
        /// Purpose:Return simulated next value for the stock code.
        /// </summary>
        /// <param name="stockCode"></param>
        /// <returns>
        public StockFeed GetNextFeed(string stockCode) {

            if (_stockIndexList.ContainsKey(stockCode))
            {
                int index = _stockIndexList[stockCode];
                List<StockFeed> feedList = _stockFeed[stockCode];
                if (index >= feedList.Count-1)
                {
                    index = 0;

                }
                _stockIndexList[stockCode] = ++index;
                StockFeed sf= feedList[index];
                sf.FeedIndex = index;
                return sf;
            }
            return null;
        }
    }
}  
</returns>

From the above code you can see that the FeedGenerator class defines following key fields. _stockFeed is a dictionary maintaining the collection of StockFeed initialised by reading from CSV files. _stockFeedIndexList is the dictionary maintaining the index of the feed last provided for each stocks. This class has one private method InitFeed() which populates the dictionary. ExtractStockFeedFromLine() is another private method and it is a helper method to parse the line read from CSV and initialise StcokFeed entity. This class exposes one public method GetNextFeed() which provides the new value of a feed.

Implementing StockFeedObservable

StockFeedObservable class is responsible for publishing stockPriceChangedEvent. It exposes following key methods Start() and Stop(). The Start() method spawns a thread in a background and gets new feed by invoking the GetNextFeed() method of FeedGenerator. It then raises the stockPriceChanged event to notify all the observers. Below code shows the implementation details. This class is implemented as a Singleton.

 /// <summary>
 /// Class which is the main class and act as Observer of Stock Feed
 /// Notifies all the subscribers about stock Feed.
 /// This class is implemented as a singleton
 /// </summary>
public class StockFeedObservable
 {
    private static readonly StockFeedObservable _instance = new StockFeedObservable();
    /// <summary>
    /// Property:stockPriceChanged event.
    /// </summary>
    public event EventHandler<StockFeedEventArgs> stockPriceChanged;

    private FeedGenerator fg = new FeedGenerator();
    string[] _files = { "RELCAP", "ITC", "INFY", "TCS" };
    private int _fileIndex=0;
    private bool _simFlag = false;
    System.Threading.Thread newTheread = null;
    private StockFeedObservable() {

        fg.InitFeed(_files);
    }

    /// <summary>
    /// Property:Observer
    /// Returns the instance of Singleton class
    /// </summary>
    public static StockFeedObservable Observer {

        get { return _instance; }
    }

    /// <summary>
    /// Method:Start
    /// Purpose:Starts simulating feed in new Thread
    /// </summary>
    public void Start()
    {

         newTheread= new System.Threading.Thread(simulateFeed);
         _fileIndex = 0;
         _simFlag = true;
          newTheread.Start();

    }

    /// <summary>
    /// Method:Stop
    /// Purpose:Stops the thread.
    /// </summary>
    public void Stop() {

        if (newTheread != null)
            _simFlag = false;
    }

    /// <summary>
    /// Method:SimulateFeed
    /// Purpose:Simulates the feed.
    /// </summary>
    /// <param name="obj"></param>
    private void simulateFeed(object obj)
    {
        while (_simFlag)
        {
            if (_fileIndex >= _files.Length)
            {
                _fileIndex = 0;
            }
            StockFeed sf = fg.GetNextFeed(_files[_fileIndex]);
            StockFeedEventArgs args = new StockFeedEventArgs();
            args.SF = sf;
            args.StockCode = _files[_fileIndex];
            OnPriceChanged(args);
            _fileIndex++;
            System.Threading.Thread.Sleep(100);

        }
    }

    /// <summary>
    /// Method:OnPriceChanged
    /// Purpose:Raises the price changed event.
    /// </summary>
    /// <param name="args"></param>
    private void OnPriceChanged(StockFeedEventArgs args) {

        if (stockPriceChanged != null)
            stockPriceChanged(this,args);
    }




 }

The above code is self explanatory.

Implementing Observers

The detailed implementation of each type of observers is given below.

TrendGraphControl Implementation

The TrendGraphControl is a conceptual class and since the GraphControl class displays real time trend of stock feed, I have given it this name. This control uses WPF GDI Graphics capabilities to display the chart control and plots the trend in real time. Since this is one of our observer type, hence it implements IObserver interface. It implements Update() method to plot the stock feed trend in real time. Below code shows the implementation of the key methods. This class is also derived from UserControl class.

     /// <summary>
    /// Interaction logic for TrendView.xaml
    /// </summary>
    public partial class GraphControl : UserControl,IObserver
    {
       /// <summary>
       /// Constructor for TrendView
       /// </summary>
        public GraphControl()
        {
            InitializeComponent();
            _dataPoints = new List<point>();
            _orgDataPoints = new List<point>();
            _minY = -40.0;
            _maxY = 40.0;
        }


        /// <summary>
        /// Property:StockCode
        /// </summary>
        public string StockCode { get; set; }

        /// <summary>
        /// Method:OnRender
        /// Purpose:Renders the graph.
        /// </summary>
        /// <param name="drawingContext"></param>
        protected override void OnRender(DrawingContext drawingContext)
        {
            // base.OnRender(drawingContext);
            _drawingContext = drawingContext;
            DrawGraph(drawingContext);
        }




        /// <summary>
        /// Method:DrawLegend
        /// Purpose:To display Legend
        /// </summary>

        private void DrawLegend()
        {
            if (_orgDataPoints.Count == 0)
                return;
            // Display Legend between minimum and maximum.
            double range = _maxY - _minY;
            double step = range / 5.0;
        
              double curValue = _minY;
             
            for (int i = 0; i <= 5; i++)
            {
                if(i==5)
                {
                    curValue = _maxY;
                }
                Point p = new Point();
                p.X = _minX;
                p.Y = curValue;

                Point pt = TranslateToPixels(p);
                 pt.X = pt.X -40;
                 pt.Y = pt.Y - 15;
                string curValueS = string.Format("{0:0.0}", curValue);
                DrawFormattedText(curValueS, pt,9,Brushes.Blue);
                curValue += step;
            }
            
            // Display Stock Code
            Point p1 = new Point();
            p1.X = _maxX - 50;
            p1.Y = _maxY;
            Point cp1 = TranslateToPixels(p1);
            cp1.Y = _graphTop + 20;
            string stockCode = string.Format("Stock Code: {0}", _stockCode);
            DrawFormattedText(stockCode, cp1, 10, Brushes.Yellow);

        }


        /// <summary>
        /// Method:DrawFormttedText
        /// </summary>
        /// <param name="curValueS"></param>
        /// <param name="pt"></param>
        /// <param name="fontSize"></param>
        /// <param name="foregroundBrush"></param>
        private void DrawFormattedText(string curValueS,Point pt,int fontSize, Brush foregroundBrush) {

            
            FormattedText frmText = new FormattedText(curValueS,

                     CultureInfo.GetCultureInfo("en-us"),
                     FlowDirection.LeftToRight,
                     new Typeface("Verdana"),
                     fontSize,
                     foregroundBrush);
            _drawingContext.DrawText(frmText,pt);

        }
        /// <summary>
        /// Method:DrawGraph
        /// </summary>
        /// <param name="drawContext"></param>
        private void DrawGraph(DrawingContext drawContext)
        {
            _graphWidth = ActualWidth * WIDTH_FACTOR;
            _graphHeight = ActualHeight * HEIGH_FACTOR;
            Rect rect;
            Brush fillBrush;
            Pen outlinePen = new Pen(Brushes.Yellow, 1);
            fillBrush = new SolidColorBrush(Colors.Black);
            _graphLeft = ActualWidth * LEFT_MARGIN_FACTOR;
            _graphTop = ActualHeight * TOP_MARGIN_FACTOR;
            rect = new Rect(_graphLeft, _graphTop, _graphWidth, _graphHeight);
            drawContext.DrawRectangle(fillBrush, outlinePen, rect);
            //Draw Markers
            double horGap, verGap;
            horGap = _graphHeight / MAX_HOR_DIV;
            verGap = _graphWidth / MAX_VER_DIV;
            Pen markerPen = new Pen(Brushes.Green, 1);
            for (int i = 1; i < MAX_HOR_DIV; i++)
            {
                Point p1 = new Point(_graphLeft, _graphTop + (i * horGap));
                Point p2 = new Point(_graphLeft + _graphWidth, _graphTop + (i * horGap));
                drawContext.DrawLine(markerPen, p1, p2);
            }
            for (int i = 1; i < MAX_VER_DIV; i++)
            {
                Point p1 = new Point(_graphLeft + (i * verGap), _graphTop);
                Point p2 = new Point(_graphLeft + (i * verGap), _graphTop + _graphHeight);
                drawContext.DrawLine(markerPen, p1, p2);
            }

            // Draw Legend
            DrawLegend();
            // DrawCurve
            
            
            MapDataPoints();
            Pen tracePen = new Pen(Brushes.Cyan, 1);
            for (int i = 0; i < _dataPoints.Count - 1; i++)
            {
                drawContext.DrawLine(tracePen, _dataPoints[i], _dataPoints[i + 1]);
                if (i % 10 == 0) {
                    Pen circlePen = new Pen(Brushes.Yellow, 1);
                    drawContext.DrawEllipse(Brushes.Transparent, circlePen, _dataPoints[i], 2F,2F);
                }

            }
            

        }

  
    

        /// <summary>
        /// Method:Update
        /// Updates the Graph.
        /// </summary>
        /// <param name="sf"></param>
        
        private void UpdateGraph(StockFeed sf)
        {
            try
            {

                if (string.IsNullOrEmpty(_stockCode))
                    _stockCode = sf.StockCode;

                if (_orgDataPoints == null)
                    _orgDataPoints = new List<point>();
                Point p = new Point();
                p.X = ++_currrentPoint;
                p.Y = (double)sf.Price;

                if (_orgDataPoints.Count < MAX_POINTS)
                {
                    _orgDataPoints.Add(p);
                    
                }
                else
                {
                    // Shift the points by one to the left...
                    for (int i = 0; i < _orgDataPoints.Count - 1; i++)
                    {
                        Point pt = _orgDataPoints[i];
                        pt.X = i + 1;
                        pt.Y = _orgDataPoints[i + 1].Y;
                        _orgDataPoints[i] = pt;
                    }
                    p.X = MAX_POINTS;
                    _orgDataPoints[MAX_POINTS - 1] = p;
                }
                _minX = 0;
                _maxX = 250;
                _minY = _orgDataPoints.Min(pt => pt.Y)*0.7;
                _maxY = _orgDataPoints.Max(pt => pt.Y) * 1.25;
           
                this.InvalidateVisual();

            }
            catch (Exception ex)
            {
                string message;
                message = "Error occured while updating data points and error is" + ex.ToString();
                throw new Exception(message);

            }
        }



     
        /// <summary>
        /// Method:MapDataPoints
        /// Purpose:Translates all data points to pixel coordinates
        /// </summary>
        private void MapDataPoints()
        {
            // clear graph data
            _dataPoints.Clear();
            Point curPt = new Point();
            Point convPt = new Point();
            for (int i = 0; i < _orgDataPoints.Count; i++)
            {
                curPt = _orgDataPoints[i];
                convPt = TranslateToPixels(curPt);
                _dataPoints.Add(convPt);


            }

        }

        /// <summary>
        /// Method:TranslateToPixels
        /// Purpose:Translates the point to pixel coordinate to plot on Graph.
        /// </summary>
        /// <param name="p"<>/param<
        /// <returns></returns>
        private Point TranslateToPixels(Point p)
        {
            Point convPt = new Point();
            double x, y;
            x = (p.X - _minX) / (_maxX - _minX) * _graphWidth;
            //  y = ((p.Y- _minY) * _magFactorY*-1.0) / (_maxY - _minY) * _graphHeight;
            y =(_maxY - p.Y) / (_maxY - _minY) * _graphHeight;
            if (y < 0)
                y = 0;
            convPt.X = _graphLeft + Math.Min(x, _graphWidth);
            convPt.Y = _graphTop + Math.Min(y, _graphHeight);


            return convPt;

        }

        /// <summary>
        /// Method: Update
        /// Updates the trend control
        /// </summary>
        /// <param name="e"></param>
        public void Update(StockFeedEventArgs e)
        {
            if (e.StockCode.Equals(this.StockCode, StringComparison.InvariantCultureIgnoreCase))
                UpdateGraph(e.SF);
        }

        // All field variables.
        private List<Point> _dataPoints = null;
        private List<Point> _orgDataPoints = null;
        private int _currrentPoint = 0;
        private double _minX, _minY;
        private double _maxX, _maxY;
        private double _graphWidth;
        private double _graphHeight;
        private double _graphLeft;
        private double _graphTop;
        private const int MAX_POINTS = 250;
        private const int MAX_HOR_DIV = 20;
        private const int MAX_VER_DIV = 10;
        private const double WIDTH_FACTOR = 0.85;
        private const double HEIGH_FACTOR = 0.9;
        private const double TOP_MARGIN_FACTOR = 0.05;
        private const double LEFT_MARGIN_FACTOR = 0.075;
        private DrawingContext _drawingContext = null;
        private string _stockCode = string.Empty;

       
    }
</point></point></point>

From the above code details you can see that OnRender method is overridden to get the handle to DrawingContext instance. DrawingContext class exposes key methods such as DrawEllipse(), DrawLine(),DrawRectangle() methods. ActualWidth and ActualHeight properties of the FrameworkElement class provides the window size available for rendering. DrawGraph() method renders the rectangular grid and also the Trend based on StockFeed value. It makes use of Pen and Brush objects to draw the content. The StockFeed price is displayed on y axis and x-axis is time. TranslateToPixels()method converts the stock price into screen coordinates before plotting the trend. StockFeed data points are maintained in a collection and maximum size of this collection is set as 250 points. When the collection is populated to its maximum size it then shifts the data points by one to the left. left.Please refer UpdateGraph() method above for more details. Update() method is invoked whenever it gets notification of new value of a StockFeed.

Implementation of StockDashBoard

The StockDashboard is another type of observer used in this application. This class exposes some key properties such as Last Logged-in User, Last Command and Last Feed. WPF StatusBar control is used to display all these properties. I have used WPF data binding here and set the DataContext property of this control to instance of StockDashboard class. This class is implemented as a singleton. As we are using data-binding we want automatic update of status bar whenever any of the properties exposed by this class are changed. To achieve this we need to implement INotfiyPropertyChanged interface. Also since this is also an observer it also implements IObserver interface. The code details of this class are given below.

  /// <summary>
  /// StatusBoard class contains key properties to update statusbar
  /// </summary>
public  class StatusBoard:INotifyPropertyChanged,IObserver

  {

      private bool _displayFeed = false;
      private int _upodateCounter = 0;
      private string _stockFeedMessage = string.Empty;
      private string _feedMessage = string.Empty;

       private static readonly StatusBoard _instance = new StatusBoard();

      private StatusBoard()
      {

      }

    /// <summary>
    /// Property:CurrentDashBoard
    /// </summary>
      public static StatusBoard CurrentDashboard
      {
          get { return _instance; }
      }

      /// <summary>
      /// Property:LoggedInUser
      /// </summary>
      public string LoggedInUser
      {
          get { return System.Environment.UserName; }


      }

    /// <summary>
    /// Proeprty:LastCommand
    /// </summary>
     public string LastCommand
      {
          get { return _lastCommand; }
          set {
              _lastCommand = value;
              NotifyChanged("LastCommand");

          }
      }


    /// <summary>
    /// Property:LastFeed
    /// </summary>
     public string LastFeed
     {

         get { return _lastFeed; }
         set
         {
             _lastFeed = value;
             NotifyChanged("LastFeed");

         }
     }



      #region INotifyPropertyChanged Members

    /// <summary>
    /// Event PropertyChanged
    /// </summary>
      public event PropertyChangedEventHandler PropertyChanged;
      /// <summary>
      /// Method:NotifyChanged
      /// </summary>
      /// <param name="property"<>/param>
      public void NotifyChanged(string property)
      {
          if (PropertyChanged != null)
          {
              PropertyChanged(this, new PropertyChangedEventArgs(property));
          }
      }

      #endregion
      private string _lastCommand;
      private string _lastFeed;

    /// <summary>
    /// Method:Update
    /// Purpose:Interface method of IObserver and is used to update statusboard based on stock feed change.
    /// </summary>
    /// <param name="e"></param>
      public void Update(StockFeedEventArgs e)
      {
          if (!_displayFeed)
          {


              _stockFeedMessage += string.Format(" {0}:Rs. {1}", e.StockCode, e.SF.Price);
              _upodateCounter++;
              if (_upodateCounter % 4 == 0)
              {
                  string spacers = new string(' ', _stockFeedMessage.Length);
                   _feedMessagge = spacers+_stockFeedMessage+spacers;

                   Thread newThread = new Thread(showFeed);
                   _displayFeed = true;
                   newThread.Start();

              }
          }
      }

      private void showFeed(object obj)
      {
          int count = _stockFeedMessage.Length * 2;
          for (int i = 0; i < count; i++)
          {
              this.LastFeed = _feedMessagge.Substring(i, _stockFeedMessage.Length);
              Thread.Sleep(300);
          }
          _displayFeed = false;
          _stockFeedMessage = string.Empty;
      }

  }
Most of the above code is self explanatory. Whenever any property is changed then NotifyChanged method is invoked to raise the PropertyChanged event. showFeed() method is invoked in a background thread and updates the LastFeed as a rolling display.

Implementing StcokFeedXMLExporter

StockFeedXMLExporter is our last type of observer in this application and it exports the StockFeed in XML format. It groups them in a size of 250 points and then saves it to a XML file with a timestamp. Each group of 250 StockFeed points is saved in separate file. The implementation details of this class are given below.

/// <summary>
/// This is another observer who is interested in stock feed and
/// exports the feed in XML format.
/// </summary>
public class StockFeedXMLExporter:IObserver
{
    private XElement _root = new XElement("stockfeeds");
    private int _noOfChilderen = 0;
    private  const int MAX_CHILD_ELEMENTS =250;
    private string _filePath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) + @"\ObserverDemo\Export\";


    /// <summary>
    /// Method:Update
    /// Updates XML Feed
    /// </summary>
    /// <param name="e"></param>
    public void Update(StockFeedEventArgs e)
    {
        try
        {
        _root.Add(new XElement("stockfeed",
                            new XElement("stockcode",e.SF.StockCode),
                            new XElement("price",e.SF.Price),
                            new XElement("maxprice",e.SF.MaxPrice),
                            new XElement("minprice",e.SF.MinPrice),
                            new XElement("timestamp",e.SF.StockFeedDate)
                            ));
        _noOfChilderen++;
        if (_noOfChilderen >= MAX_CHILD_ELEMENTS) {
            string fileName = string.Format("{0}stockfeed{1}_{2}_{3}_{4}_{5}_{6}.xml",
            _filePath, DateTime.Now.Day, DateTime.Now.Month, DateTime.Now.Year, DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second);
            _root.Save(fileName);
            _noOfChilderen = 0;
            _root = new XElement("stockfeeds");
        }
    }catch(Exception ex){

        string message = "Error occured while updating xml file and error is " + ex.ToString();
        Logger.Log.Error(message);
    }
   }
}

From the above code you can see this is a very simple observer of all the three types and contains only Update()method. This method updates the StockFeed received as an element of stockfeeds root element. It saves this as a XML file.

Implementation of Main Application Window

Since the application is WPF based the ObserverDemoMain is a main window class and is derived from System.Windows.Window class. This GUI class acts as a main container of the application and all other GUI controls including layout panels, user controls and other GUI widgets are contained within this main window. This class is implemeted in two files Observermain.xaml which contains the markup for defining the layout and all gui elements and ObserverDemo.xaml.cs is a code behind file and acts like a controller and will contain all the handler methods to interact with GUI. This is very much similar to .aspx and aspx.cs files in ASP.NET web application. This class contains the handler for all the command buttons and menu options. In addition, this class is also responsible for initialising all the observers and maintains the list of all the observers. Since most of the observers in this application updates GUI this class subscribes to stockPriceChanged event of StockFeedObservable class. It is also responsible to start and stop the trend in response to user's invocation of these actions. Below code shows the key methods used in this class. For complete details of both the markup and full code please refer to source code provided in the download link.

private List<IObserver> _observers = new List<IObserver>();
    /// <summary>
    /// Constructor
    /// </summary>
    public Observermain()
    {
        InitializeComponent();
        mainstatusBar.DataContext = StatusBoard.CurrentDashboard;
        InitObservers();
    }

    private void InitObservers()
    {
        graphCtl.StockCode = "RELCAP";
        graphCt2.StockCode = "ITC";
        graphCt3.StockCode = "INFY";
        graphCt4.StockCode = "TCS";

        _observers.Add(graphCtl);
        _observers.Add(graphCt2);
        _observers.Add(graphCt3);
        _observers.Add(graphCt4);
        _observers.Add(StatusBoard.CurrentDashboard);
        _observers.Add(new StockFeedXMLExporter());

    }




    /// <summary>
    /// Method:Update
    /// Purpose:Updates each observers.
    /// </summary>
    /// <param name="e"></param>
    private void Update(StockFeedEventArgs e)
    {
        if (e.SF != null)
        {

            _observers.ForEach(o => o.Update(e));

        }

    }


   /// <summary>
   /// Method:Update_StockPrice
   /// Purposse:Invokes the Dispatcher.Invoke to invoke method which updates the GUI in sync with User Interface Thread.
   /// </summary>
   /// <param name="sender"></param>
   /// <param name="e"></param>
    private void Update_StockPrice(object sender, StockFeedEventArgs e)
    {
        Dispatcher.Invoke((Action)(()=>Update(e)));


    }


    /// <summary>
    /// Purpose:Handler for StartObserver command
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void StartObserve_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        StockFeedObservable.Observer.stockPriceChanged += Update_StockPrice;
        StockFeedObservable.Observer.Start();
        StatusBoard.CurrentDashboard.LastCommand = "Observe Trend";


    }

    /// <summary>
    /// Purpose:Handler to enable/disable the command
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void StartObserve_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        e.CanExecute=true;
    }

    /// <summary>
    /// Command handler to stop the observe trend.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void StopObserve_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        StockFeedObservable.Observer.stockPriceChanged -= Update_StockPrice;
        StockFeedObservable.Observer.Stop();
        StatusBoard.CurrentDashboard.LastCommand = "Stop Trend";

    }

From the above code you can see that, The list of observers is initialised using InitObservers() method. Also the datacontext property of StatusBar control is set to instance of StatusBoard observer. The StartObserve_Executed() method is a handler for StartObserveCommand and here it subscribes to stockPriceChanged event. As mentioned earlier since the background thread can't be used to update the GUI elements the observers have subscribed indirectly. Update_Price()method then delegates it to Update() method which is invoked using Dispatcher.Invoke() method. Also see the use of lambda expression to invoke Update() methods of each of the observers.

Role of Dispatcher.Invoke() Method

In WPF, only the thread that created a DispatcherObject may access that object. For example, a background thread that is spun off from the main UI thread cannot update the contents of a GUI elements that were created on the UI thread. In order for the background thread to access this, the background thread must delegate the work to the Dispatcher associated with the UI thread. This is accomplished by using either Invoke or BeginInvoke. Invoke is synchronous and BeginInvoke is asynchronous. The operation is added to the event queue of the Dispatcher at the specified DispatcherPriority. Invoke is a synchronous operation; therefore, control will not return to the calling object until after the callback returns.

Data Binding

Below markup shows the data-binding syntax used to update the StatusBar control.

<StatusBar Grid.Row="3"  Grid.Column="0" Grid.ColumnSpan="2" Name="mainstatusBar" HorizontalAlignment="Stretch" Background="{StaticResource statusbarBackgroundBrush}" >
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch" >
                <TextBlock Style="{StaticResource headerTextBlock}" Margin="3" VerticalAlignment="Center" Text="User: " />

                <TextBlock Style="{StaticResource headerTextBlock}" Margin="3" Name="txtLoggedInUser" Text="{Binding Path=LoggedInUser}" MinWidth="150" HorizontalAlignment="Left"  />
                <TextBlock Style="{StaticResource headerTextBlock}" Margin="3" VerticalAlignment="Center" Text="Last Command: " />
                <TextBlock Style="{StaticResource headerTextBlock}" Margin="3" Name="txtLastCommand" Text="{Binding Path=LastCommand}" MinWidth="150" HorizontalAlignment="Left"  />
                <TextBlock Style="{StaticResource headerTextBlock}" Margin="3" VerticalAlignment="Center" Text="Last Feed: " />
                <TextBlock Style="{StaticResource headerTextBlock}" Margin="3" Name="txtLastFeed" Text="{Binding Path=LastFeed}" MinWidth="150" HorizontalAlignment="Left"  />

            </StackPanel>
        </StatusBar>

From the above mark-up please see the data-binding syntax. The Text property is bind to public properties of statusBoard instance which is set as DataContext property of StatusBar control.

WPF Themes

We can use WPF Themes to style the controls used in WPF application. You can define new theme as XAML file. Below markup shows sample theme for Brush object. The themes used in this application under app.xaml as it's defined under it's application resources. The sample markup in app.xaml is shown below. Please refer the source code provided in download link for more details.

<ResourceDictionary
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml<Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Themes\Brushes.xaml" />
                <ResourceDictionary Source="Themes\General.xaml" />
                <ResourceDictionary Source="Themes\Toolbar.xaml" />
                <ResourceDictionary Source="Themes\DataGrid.Generic.xaml" />
               
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>  
    </Application.Resources>gt;

    <!-- Generic brushes #3F5478 -->
    <SolidColorBrush x:Key="DefaultControlBorderBrush" Color="#FF688CAF"/>
    <SolidColorBrush x:Key="DefaultControlBackgroundBrush" Color="#FFE3F1FE"/>
    <SolidColorBrush x:Key="DefaultControlForegroundBrush" Color="#FF10257F"/>
    <SolidColorBrush x:Key="DefaultBorderBrush" Color="#3F5478"/>
    <SolidColorBrush x:Key="DefaultBackgroundBrush" Color="#BCC7D8"/>
    <SolidColorBrush x:Key="borderBackgroundBrush" Color="#3F5478"/>
    <SolidColorBrush x:Key="statusbarBackgroundBrush" Color="#3F5478"/>
    <!--<SolidColorBrush x:Key="ListBoxBackgroundBrush" Color="White"/>-->
    <SolidColorBrush x:Key="HighLightBackgroundBrush" Color="#3F5478"/>
    <SolidColorBrush x:Key="DisabledBackgroundBrush" Color="LightGray"/>
    <SolidColorBrush x:Key="HighLightBorderBrush" Color="Orange"/>
</ResourceDictonary>

Command Binding

You can use pre-defined application commands or define custom commands. These commands then can be linked to toolbar buttons or menu items using WPF data-binding. You can define Command_Executed() and Can_Execute() handlers for these commands. In Can_Execute() handler you can define logic to enable/disable command button. Please see the code for more detail of markup and code used for defining custom commands. All the commands are defined as instance of RoutedUICommand

Implementation of Logger class

Purpose of this class is just to demonstrate the application of Singleton design pattern.You can use Logging Application Block provided by Enterprise Library available for download on Microsoft's Patteren and Practice Website. You can read my article Implementing Custom Action Pane and Custom Ribbon for Excel 2010 using VSTO and C# for detailed implementation details of this class.

Download Source Code and Data Files

Please download the DownloadData.zip using the download links provided at the top of this article. Please extract the zip file in your MyDocuments folder. So it creates the folder structure and also copies the data files required for simulating the StockFeed. Folder structure under MyDocuments folder is given below.

  • ObserverDemo
    • Stocks (This folder contains all data files)
    • Logs (This folder keeps all the log files)
    • Export (This folder keeps the XML file exported)

Points of Interest

I have created chm help file from XML document created from the source code comments. I have used SandCastle Help File Builder to build the help file. I have integrated the help file in this application and can be invoked from the help menu option. You can generate XML document by right clicking on solution and select the project properties and tick the XML documentation option in output under build tab. Please see the screen shot shown in figure 3.

Option for XML Documentation

Figure 3: Dialog Box for setting output option for generating XML document.

I have published this application as one-click deployment. You can download the application setup using link here. Also you need to download the data files before running this application as the data files are pre-requisite to simulate the StockFeed.

Those who are interested in other design patterns then you use below links for more details.

Conclusion

Design patterns provide solution to recurring problems faced in software development. Observer is popular design pattern and I hope this article will provide you the insight of how this pattern works and its application in real life scenarios. Also WPF is a very powerful framework provided by Microsoft and enables us to develop powerful GUI and is a new alternative to older win-form based applications. I hope you will enjoy reading this article. If you have any queries and need more information then you can e-mail me. Thank you.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)