实时股票行情仪表板(使用 WPF 和 C# 应用观察者设计模式)
本文展示了观察者设计模式在现实场景中的应用。
背景
在软件开发中,我们经常会遇到在不同项目中实现过的场景。设计模式是针对这些常见问题的解决方案,面向对象编程的设计者提供了各种设计模式,使我们能够以最佳方式解决特定问题。在本文中,我将向您展示最流行的设计模式之一 Observer
的应用及其在现实场景中的应用。有关设计模式的更多信息,请参阅此 链接。
观察者是一种设计模式,有时也称为发布/订阅模式,当多个利益相关者可能对某个特定主题感兴趣时,它非常有用。他们可以订阅它。当主题的状态发生变化时,它会通知其订户,订户会更新其级别。本文应用场景为实时股票信息仪表盘,这对于股票经纪人和投资者尤其重要,他们可能对实时监控自己感兴趣的关键股票的走势感兴趣。
引言
本文的目的是演示观察者设计模式在现实场景中的应用。在本文中,我以实时股票信息仪表盘为例,展示了四只关键股票的实时走势。在此示例中,我采用了三种类型的观察者,它们对 StockFeed
的变化感兴趣,并以不同的方式更新或使用它们。下面列出了此示例中使用的三种观察者类别:
- 实时趋势控件 (4 个视图)
- 应用程序状态栏中股票信息的滚动显示。
- StockFeedXML 导出器。此工具将股票信息导出为 XML 格式。
本文将使用 WPF 和 C# 提供此模式的详细实现。在本文中,我还将探讨 WPF 的以下功能:
- 使用不同的布局面板
- 数据绑定
- WPF 中的主题
- WPF 的图形功能
- 工具栏和命令设计模式的使用
- Lambda 表达式
设计概述
图 1 显示了实时股票仪表盘应用程序的类图。由于大多数观察者正在更新 GUI 元素,而 GUI 元素不允许从不同线程更新,因此更改会间接通知所有观察者。这是在 Dispatcher.Invoke
方法期间完成的,我将在后面的部分详细介绍此方法的用途。
图 1:演示应用程序的基本类图
从类图来看,IObserver
是一个接口,所有观察者 TrendGraphControl
、StatusBoard
和 StockFeedXMLExporter
都实现了它。StockFeedObservable
是一个负责发布 stockPriceChanged
事件的类,所有感兴趣的观察者都将订阅此事件。FeedGenerator
是一个负责模拟数据源并提供新的 StockFeed
值的类。StockFeedObservable
类在后台运行一个线程,通过调用 FeedGenerator
类的 GetNextFeed()
方法来接收新的 StockFeed
值。StockFeedObservable
类在接收到新数据后会引发事件,并通知其所有订阅者。StockFeedEventArgs
是一个包装 StockFeed
的类。所有具体的观察者在收到通知后都会自行更新。
业务层
StockFeed
是此应用程序的主要模型。所有观察者都对这个主题感兴趣。我将详细解释 FeedGenerator
类的实现,它充当提供新数据的源。
FeedGenerator 的实现
FeedGenerator
是一个负责为不同股票提供股票信息的类。在本应用程序中,我使用了一些虚构的数据来模拟关键股票的信息数据,这些关键股票的源数据存储在 CSV 文件中。FeedGenerator
类然后读取这些文件并将它们维护为 StockFeed
实体的集合。由于有四种不同的股票,因此每种股票的集合都维护在一个字典中。GetNextFeed()
方法随后从该集合中返回新的 StockFeed
值。实现细节如下。
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> ///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; } } }
从上面的代码可以看出,FeedGenerator
类定义了以下关键字段。_stockFeed
是一个字典,维护通过从 CSV 文件读取初始化的 StockFeed
集合。_stockFeedIndexList
是一个字典,维护为每种股票最后提供信息的索引。该类有一个私有方法 InitFeed()
,用于填充字典。ExtractStockFeedFromLine()
是另一个私有方法,它是一个辅助方法,用于解析从 CSV 读取的行并初始化 StcokFeed
实体。该类公开了一个公共方法 GetNextFeed()
,用于提供新的信息值。
实现 StockFeedObservable
StockFeedObservable
类负责发布 stockPriceChangedEvent
。它公开了以下关键方法:Start()
和 Stop()
。Start()
方法在后台启动一个线程,并通过调用 FeedGenerator
的 GetNextFeed()
方法来获取新数据。然后,它会引发 stockPriceChanged
事件来通知所有观察者。下面的代码显示了实现细节。此类实现为 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); } }
上面的代码不言自明。
实现观察者
每种观察者的详细实现如下。TrendGraphControl 实现
TrendGraphControl
是一个概念类,由于 GraphControl
类显示股票信息的实时趋势,因此我给它起了这个名字。此控件使用 WPF GDI 图形功能来显示图表控件并实时绘制趋势。由于这是我们的观察者类型之一,因此它实现了 IObserver
接口。它实现了 Update()
方法来实时绘制股票信息趋势。下面的代码显示了关键方法的实现。此类也派生自 UserControl
类。
/// <summary> /// Interaction logic for TrendView.xaml /// </summary> public partial class GraphControl : UserControl,IObserver { /// <summary> /// Constructor for TrendView /// </summary> public GraphControl() { InitializeComponent(); _dataPoints = new List(); _orgDataPoints = new List (); _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 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; }
从上面的代码细节可以看出,OnRender
方法被重写以获取 DrawingContext
实例的句柄。DrawingContext
类公开了诸如 DrawEllipse()
、DrawLine()
、DrawRectangle()
等关键方法。FrameworkElement
类的 ActualWidth
和 ActualHeight
属性提供了可用于渲染的窗口大小。DrawGraph()
方法渲染矩形网格以及基于 StockFeed
值的 Trend
。它利用 Pen
和 Brush
对象来绘制内容。StockFeed
价格显示在 y 轴上,x 轴是时间。TranslateToPixels()
方法在绘制趋势之前将股票价格转换为屏幕坐标。StockFeed
数据点存储在一个集合中,该集合的最大大小设置为 250 个点。当集合填充到最大大小时,它会将数据点向左移动一个位置。请参阅上面的 UpdateGraph()
方法了解更多详细信息。每当收到新的 StockFeed
值通知时,都会调用 Update()
方法。
StockDashboard 的实现
StockDashboard
是此应用程序中使用的另一种观察者类型。该类公开了一些关键属性,例如 Last Logged-in User
、Last Command
和 Last Feed
。WPF StatusBar
控件用于显示所有这些属性。我在这里使用了 WPF 数据绑定,并将此控件的 DataContext
属性设置为 StockDashboard
类实例。此类实现为单例。由于我们使用数据绑定,因此希望在类公开的任何属性更改时自动更新状态栏。为此,我们需要实现 INotfiyPropertyChanged
接口。同样,由于它也是一个观察者,因此它也实现了 IObserver
接口。此类代码详情如下。
/// <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; } }上面的代码大部分是不言自明的。每当更改任何属性时,都会调用
NotifyChanged
方法来引发 PropertyChanged
事件。showFeed()
方法在一个后台线程中调用,并将 LastFeed
更新为滚动显示。实现 StockFeedXMLExporter
StockFeedXMLExporter
是我们应用程序中的最后一个观察者类型,它将 StockFeed
导出为 XML 格式。它将它们分组为 250 个点的大小,然后使用时间戳将其保存到 XML 文件中。每 250 个 StockFeed
点分组保存到一个单独的文件中。此类实现细节如下。
/// <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); } } }
从上面的代码可以看出,这是三种类型中最简单的观察者,只包含 Update()
方法。此方法将接收到的 StockFeed
作为 stockfeeds
根元素的元素进行更新。它将其保存为 XML 文件。
主应用程序窗口的实现
由于该应用程序是基于 WPF 的,因此 ObserverDemoMain
是主窗口类,它派生自 System.Windows.Window
类。这个 GUI 类充当应用程序的主容器,所有其他 GUI 控件,包括布局面板、用户控件和其他 GUI 小部件,都包含在此主窗口中。此类在两个文件中实现:Observermain.xaml
包含定义布局和所有 GUI 元素的标记,ObserverDemo.xaml.cs
是代码隐藏文件,充当控制器,包含所有与 GUI 交互的处理程序方法。这非常类似于 ASP.NET Web 应用程序中的 .aspx
和 aspx.cs
文件。此类包含所有命令按钮和菜单选项的处理程序。此外,此类还负责初始化所有观察者并维护所有观察者的列表。由于应用程序中的大多数观察者都会更新 GUI,因此此类订阅了 StockFeedObservable
类的 stockPriceChanged
事件。它还负责响应用户调用这些操作来启动和停止趋势。下面的代码显示了此类中使用的方法。有关标记和完整代码的详细信息,请参阅下载链接中提供的源代码。
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"; }
从上面的代码可以看出,观察者列表使用 InitObservers()
方法进行初始化。此外,StatusBar
控件的 dataContext 属性设置为 StatusBoard
观察者实例。StartObserve_Executed()
方法是 StartObserveCommand
的处理程序,在此处订阅 stockPriceChanged
事件。如前所述,由于不能使用后台线程更新 GUI 元素,因此观察者以间接方式订阅。然后,Update_Price()
方法将其委托给 Update()
方法,该方法使用 Dispatcher.Invoke()
方法调用。另请注意使用 lambda 表达式调用每个观察者的 Update()
方法。
Dispatcher.Invoke() 方法的作用
在 WPF 中,只有创建 DispatcherObject 的线程才能访问该对象。例如,从主 UI 线程生成的后台线程无法更新在 UI 线程上创建的 GUI 元素的内容。为了让后台线程访问此,后台线程必须将工作委派给与 UI 线程关联的 Dispatcher。这可以通过使用 Invoke 或 BeginInvoke 来完成。Invoke 是同步的,BeginInvoke 是异步的。操作以指定的 DispatcherPriority 添加到 Dispatcher 的事件队列中。Invoke 是一个同步操作;因此,在回调返回之前,控制不会返回到调用对象。
数据绑定
下面的标记显示了用于更新 StatusBar
控件的数据绑定语法。
<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>
从上面的标记来看,请注意数据绑定语法。Text
属性绑定到设置为 StatusBar
控件的 DataContext
属性的 statusBoard
实例的公共属性。
WPF 主题
我们可以使用 WPF 主题来设置 WPF 应用程序中使用的控件的样式。您可以将新主题定义为 XAML
文件。下面的标记显示了 Brush
对象的示例主题。此应用程序中使用的主题在 app.xaml
中,因为它定义在其应用程序资源下。app.xaml
中的示例标记如下。有关更多详细信息,请参阅下载链接中提供的源代码。
<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>
命令绑定
您可以使用预定义的应用程序命令,也可以定义自定义命令。然后可以使用 WPF 数据绑定将这些命令链接到工具栏按钮或菜单项。您可以为这些命令定义 Command_Executed()
和 Can_Execute()
处理程序。在 Can_Execute()
处理程序中,您可以定义逻辑来启用/禁用命令按钮。有关定义自定义命令的标记和代码的详细信息,请参阅代码。所有命令都定义为 RoutedUICommand
的实例。
Logger 类的实现
此类仅用于演示单例设计模式的应用。您可以使用 Enterprise Library 提供的 Logging Application Block,该块可从 Microsoft 的 Patterns & Practices 网站下载。您可以阅读我的文章 使用 VSTO 和 C# 实现 Excel 2010 的自定义操作窗格和自定义功能区 以获取此类实现的详细信息。
下载源代码和数据文件
请使用本文顶部提供的下载链接下载 DownloadData.zip。请将 zip 文件解压到您的 MyDocuments
文件夹中。这样就会创建文件夹结构,并复制模拟 StockFeed
所需的数据文件。MyDocuments
文件夹下的文件夹结构如下。
- ObserverDemo
- Stocks (此文件夹包含所有数据文件)
- Logs (此文件夹保存所有日志文件)
- Export (此文件夹保存导出的 XML 文件)
关注点
我已使用 SandCastle Help File Builder SandCastle Help File Builder 从源代码注释生成的 XML 文档创建了 chm
帮助文件。我已将帮助文件集成到此应用程序中,可以从帮助菜单选项调用。您可以右键单击解决方案并选择项目属性,然后在“生成”选项卡下的“输出”中勾选“XML 文档选项”来生成 XML 文档。请参阅图 3 中所示的屏幕截图。
图 3:用于设置生成 XML 文档的输出选项的对话框。
我已将此应用程序发布为一键部署。您可以使用此处的 链接 下载应用程序安装程序。在运行此应用程序之前,您还需要下载数据文件,因为数据文件是模拟 StockFeed
的先决条件。
对于那些对其他设计模式感兴趣的人,您可以使用以下链接获取更多详细信息:
结论
设计模式为软件开发中遇到的重复性问题提供了解决方案。观察者是一种流行的设计模式,我希望本文能为您提供有关此模式工作原理及其在现实场景中应用的见解。此外,WPF 是 Microsoft 提供的功能强大的框架,它使我们能够开发强大的 GUI,并且是旧的 WinForm 应用程序的新替代方案。我希望您会喜欢阅读本文。如果您有任何疑问或需要更多信息,可以 给我发邮件。谢谢。