Prev topic: .Net Grid tutorial (Part18: Order book)
Some financial applications contain BasketViewer component designed to display financial instruments in the basket. Prices of these instruments may change in real time resulting in changes of their basket weight.
This section contains a simplified example of such application. Let us define the requirements to this component.
- Data should be displayed on 3 hierarchy levels. The top level displays instrument classes (shares, bonds, etc), the second level displays instrument categories and the last level displays the instruments.
- Financial instruments should be displayed with their current price and basket weight (as percentage).
- Instrument categories should also display market value and basket weight of the category.
- The same requirement applies to instrument classes.
- Values of individual elements may change in real time. When price of an instrument is changed, its basket weight is also changed. This results in weight changes of other instruments, categories and instrument classes.
- All information should be displayed using relevant formats for convenient data presentation.
- Basket viewer should also store time history of price changes of individual financial instruments.
- The application performance should be as high as possible.
Having defined component requirements, let’s switch to the data model. Separation of data layer from grid presentation is known to be a good style of programming.
Let’s review a simple example of a share that has a name, ISIN, price and quantity. These attributes are sufficient for displaying its base values, but not sufficient to determine its weight in instrument category and instrument basket.
The easiest way to calculate weight of instrument categories, store intermediate data and intermediate calculation results is to introduce a new Industry class, which would be an instrument category. This class can be used to store instruments from a corresponding group and results of intermediate calculations. To join instrument categories of the same class, we shall introduce InstrumentClass containing a collection that consists of instrument categories, which, in its turn, contain instruments (Share, Bond, …). Then we shall add Basket class containing collection of InstrumentClass. These classes represent business logic of the application that doesn’t know anything of its own presentation. The best way of building an application is to use an event-driven model implementing INotifyPropertyChanged interface by business logic classes.
Let’s provide a chart of classes that describe business model of the application.
One of requirements to this application is high performance. Knowledge of painting system in Windows is needed to meet this requirement. This system is asynchronous and data is painted in two stages.
- It is necessary to define area for repainting and to call Control.Invalidate(Rectangle) method.
- After some time (but not more often than every 15 msec) Windows generates WM_PAINT message. Framework .Net transforms this message to Control.Paint event that transmits Graphics objects and area for repainting.
As we see, this is quite a simple approach, however in reality Windows does a lot of work that is not always observed. As shown above, repainting requires calling Invalidate() method that is not lockable. This method can be called multiple times with different areas for repainting. However, WM_PAINT message is generated only once.
When price of any instrument changes, it is necessary to perform extensive calculations to determine weight of the changed instrument and other instruments in the basket. (For example, if instrument price drops, its weight reduces, while weight of other instruments increases). This way, if we recalculate weight every time instrument price is changed, the system will quickly reach its limit. Moreover, this is not necessary as Windows generates WM_PAINT message not less than every 15 ms. This means that multiple recalculations between these 2 messages are not needed. To better clarify the above, we provide a time chart of painting in Windows operating system below. Change of instrument price on chart corresponds to calling Control.Invalidate() method.
The above image shows that calculations are best performed at the time of painting. This brings the following advantages:
- No unneeded calculations when results cannot be displayed on screen
- Calculations are performed at the time of data displaying. Therefore, most accurate value shall be displayed.
- Repainting is performed only in visible component area. In other words, the control requests values only for visible objects. Accordingly, no calculations are performed for objects outside visible area.
- If data invalidation occurs at time intervals exceeding 15 ms, application response will be almost immediate. It’s worth mentioning that this working mode is regular for most applications including financial ones.
- If load and data update frequency increases, WM_PAINT is generated less frequently thus reducing number of labor-intensive calculations. Windows naturally regulates this creating a system with load balancing. The application will keep responding to user actions in most critical situations almost without increasing keyboard or mouse response time.
When displaying data, the grid (in WM_PAINT message) calls data objects in visible area. To put is simple, it calls for their properties. For Share type objects market value is calculated as a product of price and quantity. This operation is fairly simple. For Industry objects, market value is a total of market values of all shares of corresponding industry. If market value of one of shares is changed, industry value is also changed. Of course, this is a resource-demanding operation. However, knowing specifics of OS Windows, you can create cache for Industry object that will be cleared on price change of any shares and calculated on first call of Industry.MarketValue getter. These getters are called directly at the time of data painting. In our event-driven model, industries subscribe to their shares but don’t perform any actions in the event handler except for notifying of market value changes via INotifyPropertyChanged interface. Please note that the value itself is not calculated at this stage.
The same approach is applicable to higher InstrumentClass level. .Net Grid is an event-driven component, which means that it subscribes to notification of business logic objects. Having received a notification it calculates area for repainting and calls Control.Invalidate(Rectangle). In WM_PAINT message the grid performs a reverse operation and calculates data objects with changed values based on known area for repainting. It calls property getters of these objects that, in their turn, start calculating values and updating cache.
An example of cache implementation in Industry class is provided below:
C# | Copy |
---|---|
public class Industry : INotifyPropertyChanged { private decimal? _marketValue; private readonly ThreadSafeBindingList<Share> _shares = new ThreadSafeBindingList<Share>(); public decimal MarketValue { get { //Cache purposes: compute industry's market value if (_marketValue == null) { _marketValue = 0; foreach (Share share in _shares) { _marketValue += share.MarketValue; } } return _marketValue.Value; } } public void AddShare(Share share) { if (share != null) { share.PropertyChanged += OnSharePropertyChanged; _shares.Add(share); //Invalidate the cache _marketValue = null; } } private void OnSharePropertyChanged(object sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { case "Price": //Invalidate the cache _marketValue = null; //Fire notifications FirePropertyChanged("MarketValue"); FirePropertyChanged("MarketValueEur"); break; } } } |
This way we managed to create a system that efficiently uses computer resources limiting calculation volume and performing calculation only with visible data. Knowing market value of financial instruments you can calculate weight of each class, industry and instrument in the basket.
Hierarchy organization
Upon creation of data model, it has to be displayed in the grid. Declarative binding is the most convenient way of doing it. Let’s briefly remind you what it’s all about. Objects that have properties returning collections of other objects (for example, Industry returns Share collection) can be marked with HierarchicalFieldAttribute special attribute telling the grid that value returned by this property should be interpreted in a special way. For data collections the grid will build a hierarchy. If the collection implements INotifyPropertyChanged interface, the grid will be subscribed to changes of this collection. In other words, if a new Share is added to Industry, the grid gets a notification and automatically adds a new object at the corresponding hierarchy level. A hierarchy built this way may have any level of complexity. This approach can also be applied to remaining classes:
Copy | |
---|---|
public class Share : INotifyPropertyChanged { //... } public class Industry : INotifyPropertyChanged { private readonly ThreadSafeBindingList<Share> _shares = new ThreadSafeBindingList<Share>(); //Declarative databinding indicates grid to build hierarchy [HierarchicalField] public ThreadSafeBindingList<Share> Shares { get { return _shares; } } } public class InstrumentClass : INotifyPropertyChanged { private readonly ThreadSafeBindingList<Industry> _industries = new ThreadSafeBindingList<Industry>(); //Declarative databinding indicates grid to build hierarchy [HierarchicalField] public ThreadSafeBindingList<Industry> Industries { get { return _industries; } } } public class Basket { private readonly ThreadSafeBindingList<InstrumentClass> _instrumentClasses = new ThreadSafeBindingList<InstrumentClass>(); public ThreadSafeBindingList<InstrumentClass> InstrumentClasses { get { return _instrumentClasses; } } //... } //Set up the data source Basket basket = ...; grid.DataSource = basket.InstrumentClasses; |
A resulting image is shown below:
Data formatting
Data formatting is a simple task in Dapfor framework. It can be implemented by means of declarative formatting.
C# | Copy |
---|---|
//Share class public class Share : INotifyPropertyChanged { [Format("### ### ###.00")] public decimal LastPrice { get { ... } } [Format("### ### ###.00")] public decimal Price { get{...} set{...} } [Format("### ### ###")] public long Quantity { get{...} } [Format("#.00", null, " %")] public decimal MarketPartPercent { get{...} } [Format("### ### ###.00", null, " €")] public decimal MarketValueEur { get{...} } [Format("### ### ###.00")] public decimal MarketValue { get{...} } } //Industry class public class Industry : INotifyPropertyChanged { [Format("### ### ###.00")] public decimal MarketValue { get{...} } [Format("### ### ###.00", null, " €")] public decimal MarketValueEur { get{...} } [Format("#.00", null, " %")] public decimal MarketPartPercent { get{...} } } //InstrumentClass class public class InstrumentClass : INotifyPropertyChanged { [Format("### ### ###.00", null, " €")] public decimal MarketValueEur { get{...} } [Format("### ### ###.00")] public decimal MarketValue { get{...} } [Format("#.00", null, " %")] public decimal MarketPartPercent { get{...} } } |
If format declaration cannot be used for any reason, formats can also be set directly in columns:
C# | Copy |
---|---|
column.Format = new DoubleFormat(2); |
Data filtering
Data filtering is one of the most important elements of BasketViewer. It is especially important when working with large volumes of information. Let’s provide an example of how this functionality can be implemented in the grid. One of the ways of data filtering is implementation of IFilter interface. It is simple and contains only one method that should return a value indicating whether the specified row should be filtered or not. This method is called when adding new data, replacing a filter or receiving a notification from INotifyPropertyChanged. We shall use ISIN of financial instruments as information for filter. If all financial instruments are filtered, it is also necessary to hide an industry they belong to. To enable wildcards (?,* symbols), we use RegEx class that implements such features.
C# | Copy |
---|---|
grid.Filter = new Filter(delegate(Row row) { if (!string.IsNullOrEmpty(_regExpression)) { if(row.DataObject is Share) { //Hide share if it doesn't match to regular expression return !IsMatch((Share) row.DataObject); } if(row.DataObject is Industry) { //Hide industry if no shares match to regular expression Industry industry = (Industry) row.DataObject; foreach (Share share in industry.Shares) { if(IsMatch(share)) return false; } return true; } } return false; }); |
Data is filtered by property that doesn’t change its value during application execution. Filtering objects that change in real time doesn’t impact the code. The only difference is that IFilter.IsFiltered()()()() method will be called every time after receiving notification from INotifyPropertyChanged. Therefore, if a new data object value meets filtering conditions, this object automatically becomes visible.
Appearance
Now let’s talk a little bit about appearance. An example of simple code that changes data appearance to more user-friendly is provided below.
C# | Copy |
---|---|
grid.Appearance.VerticalLines = true; grid.Appearance.HorizontalLines = true; grid.Appearance.HorizontalFullSizeLines = false; grid.Hierarchy.ButtonBehaviour = ExpansionButtonBehaviour.HideAlways; grid.Hierarchy.ButtonWidth = 0; grid.PaintCell += delegate(object sender, PaintCellEventArgs e) { //Prevent from vertical lines painting in 1st and 2nd levels if(e.Cell.Row.Level < 2) { e.Parts ^= e.Parts & (PaintPart.VerticalLines | PaintPart.HorizontalLines); } if (e.Cell.Row.DataObject is Share && e.Cell.Column != null) { Share share = (Share) e.Cell.Row.DataObject; switch (e.Cell.Column.Id) { case "Price": e.ImageSettings.Alignment = ContentAlignment.MiddleLeft; if (share.Price > share.LastPrice) { e.Image = Resources.arrow_up_green; } if (share.Price < share.LastPrice) { e.Image = Resources.arrow_down_red; } e.Appearance.BackColor = Color.FromArgb(255, 255, 215, 0); break; case "LastPrice": e.Appearance.BackColor = Color.FromArgb(255, 255, 228, 196); break; } } }; grid.PaintRow += delegate(object sender, PaintRowEventArgs e) { //Set background colors: if (e.Row.DataObject is Industry) { e.Appearance.BackColor = Color.FromArgb(255, 0, 0, 139); e.Appearance.ForeColor = Color.White; } if (e.Row.DataObject is InstrumentClass) { e.Appearance.BackColor = Color.FromArgb(255, 239, 0, 0); e.Appearance.ForeColor = Color.White; } //Do default painting e.PaintAll(); e.Handled = true; //Do post painting for the whole row Industry industry = e.Row.DataObject as Industry; if (industry != null) { e.Graphics.DrawString(industry.IndustryName, e.Font, Brushes.White, e.Row.Bounds); } InstrumentClass instrumentClass = e.Row.DataObject as InstrumentClass; if (instrumentClass != null) { e.Graphics.DrawString(instrumentClass.InstrumentClassName, e.Font, Brushes.White, e.Row.Bounds); } }; |
Data history creation
Component requirements include displaying charts with price change history for each financial instrument. For this purpose, we shall implement new History property for Share. This property returns a collection of 40 last price changes. New values are added to this collection every time a setter is called for Price property. Let’s not forget to notify the grid of history update.
C# | Copy |
---|---|
public class Share : INotifyPropertyChanged { public const int MaxHistoryLength = 40; private decimal _price; private decimal _lastPrice; private readonly List<decimal> _history = new List<decimal>(); [Format("### ### ###.00")] public decimal Price { get { return _price; } set { if (!Equals(_price, value)) { _lastPrice = _price; _price = value; _history.Insert(0, value); if (_history.Count > MaxHistoryLength) { _history.RemoveAt(MaxHistoryLength); } FirePropertyChanged("Price"); FirePropertyChanged("LastPrice"); FirePropertyChanged("MarketValue"); FirePropertyChanged("MarketValueEur"); FirePropertyChanged("History"); } } } } |
For history painting we add a code that receives collection of latest price changes and draws a chart to Grid.PaintCell()()()() handler.
C# | Copy |
---|---|
grid.PaintCell += delegate(object sender, PaintCellEventArgs e) { //... if (e.Cell.Row.DataObject is Share && e.Cell.Column != null) { Share share = (Share) e.Cell.Row.DataObject; switch (e.Cell.Column.Id) { //... case "History": e.Parts &= e.Parts ^ PaintPart.Highlighting; e.PaintBackground(); e.Parts &= e.Parts ^ (PaintPart.Background | PaintPart.Text); PaintHistory(share, e.Cell, e.Graphics); break; } } }; private void PaintHistory(Share share, Cell cell, Graphics graphics) { if (share.History != null && share.History.Count > 0 && cell.VirtualBounds.Height > 0) { //Compute min and max bounds decimal min = 0; decimal max = 0; foreach (decimal d in share.History) { min = min > 0 ? Math.Min(min, d) : d; max = Math.Max(max, d); } decimal vRatio = (max - min) / cell.VirtualBounds.Height; //Add plots to paint the history List<Point> points = new List<Point>(); for (int i = 0; i < share.History.Count; ++i) { decimal v = share.History[i]; int dy = (vRatio > 0 ? (int)((v - min) / vRatio) - 1 : cell.VirtualBounds.Height / 2); dy = Math.Max(0, dy); int y = cell.VirtualBounds.Y + dy; int x = cell.VirtualBounds.Right - ((i + 1) * cell.VirtualBounds.Width / Share.MaxHistoryLength); if (points.Count == 0) { points.Add(new Point(cell.VirtualBounds.Right, y)); } points.Add(new Point(x, y)); } //Draw the graph using (Pen p = new Pen(Color.Blue)) { graphics.DrawLines(p, points.ToArray()); } } } |
Invalidation
Most part of data is repainted automatically via INotifyPropertyChanged notifications. However, this does not refer to some data, such as weight of instruments in a basket. Let’s assume that a weight of an instrument has changed. The grid gets a notification of it and repaints data in the corresponding cell. However, the grid has no information of corresponding weight changes of other instruments in the same basket. To update data, it is necessary to call Control.Invalidate(Rectangle) method for relevant area. Area calculation is a demanding task, however the grid provides a convenient Column.InvalidateCells()()()() method that calculates areas of all cells of the relevant column to be repainted. Below is an example of the code demonstrating repainting weights of all instruments in the basket.
C# | Copy |
---|---|
grid.RowUpdated += delegate(object sender, GridRowUpdateEventArgs e) { //When share's price is changed, the % Market part for all rows should be recomputed if(e.DataField.Id == "Price") { grid.Headers[0]["MarketPartPercent"].InvalidateCells(); } }; |
We have completely separated data appearance from presentation code. Now we have a distinctive data layer that can be used with convenient and understandable classes without regard to its presentation. Developers may find useful such tool as Inspector that can be used to inspect application business objects in real time. Business logic in real-world applications may be quite complicated and may have many different objects. Inspector enables navigation between these objects even if they are not displayed in any grid of the application. Besides, the inspector can be used to view (and change if possible) values of all data object properties, and not only properties that are presented in the grid. This helps better understand application execution without stopping it for break point in Visual Studio.