WPF Charts (Part 3)

Chart3

In my last post about charts I added a small feature to obtain object information of any chart element under the mouse cursor.

Today we are doing the next logical step. I added proper coordinates in axis units (here EUROs), time and percent units. You hover over the chart and you don’t have to stop at a specific point. The program tells you the position in relation to each axis. We have three of them, so make sure you read the right output. I believe this is more useful than just being able to read curve point hover events. Sometimes you want to know some values in between – in the middle of nowhere. And we are solving this problem now.

I kept the code flexible. There is barely any hard-coding. You might also realize some redundant checks. They may be stupid at this stage. But think practically; many people copy the code and change it according to their needs. You quickly forget implementing the little checks, which were not necessary in the first place.

Like in the last chart posts, we are dealing with two line curves. There is a third one today, which is generated at runtime. Have a look at the corresponding C# code. In hindsight it always gives a better understanding of XAML code. WPF is not as straight forward as WinForms. It requires some training here and there. For instance, you cannot simply change the height property of an object. You need to use the hard way via setters.

The third curve is a manual line. Use the left mouse button (OnMouseLeftButtonDown event) to draw it. When you do this for the first time, the line is added to the chart. It did not exist before and was not just hidden. The events OnMouseMove and OnMouseLeftButtonUp are used to complete the logic. All three are required to draw the line while the chart keeps on adding points in the background. A small bonus are the proper axis coordinates. Despite subsequent new points, which are timer based, the manual line remains scaled. The new curve is linked to the same axes as the first curve (search for: LineSeries lLineSeries = lChart.Series[0] as LineSeries; …. lRangeAxis = xLineSeries.ActualDependentRangeAxis as RangeAxis; … lAxisPoint = AxisPointFactory.getAxisPoint(lChart, lRangeAxis, lPoint); ).

 

<Window x:Class="Demo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Demo"
    xmlns:datavisualization="clr-namespace:System.Windows.Controls.DataVisualization;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    xmlns:chart="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    Title="Demo Window"
    Loaded ="Window_Loaded">

    <Grid>
        <TextBox Name="InfoBox" Text="" Height="Auto"/>
        <chart:Chart Name="myChart" Title="2014" Width="Auto" Height="Auto" MinWidth="400" MinHeight="300"
                     MouseMove="OnMouseMove"
                     MouseLeftButtonDown="OnMouseLeftButtonDown"
                     MouseLeftButtonUp="OnMouseLeftButtonUp">
            <chart:LineSeries Title="Volkswagen"
                              ItemsSource="{Binding Points}"
                              IndependentValueBinding="{Binding Date}"
                              DependentValueBinding="{Binding PriceVW}"
                              MouseMove="OnMouseMove">
                <chart:LineSeries.DependentRangeAxis>
                    <chart:LinearAxis Orientation="Y" Title="Volkswagen" ShowGridLines="True" />
                </chart:LineSeries.DependentRangeAxis>
                <chart:LineSeries.DataPointStyle>
                    <Style TargetType="{x:Type chart:LineDataPoint}">
                        <Setter Property="Background" Value="Red" />
                        <Setter Property="Height" Value="0"/>
                        <Setter Property="Width" Value="0"/>
                    </Style>
                </chart:LineSeries.DataPointStyle>
            </chart:LineSeries>

            <chart:LineSeries Title="Daimler"
                              ItemsSource="{Binding Points}"
                              IndependentValueBinding="{Binding Date}"
                              DependentValueBinding="{Binding PriceDaimler}">
                <chart:LineSeries.DependentRangeAxis>
                    <chart:LinearAxis Orientation="Y" Title="Daimler" />
                </chart:LineSeries.DependentRangeAxis>
                <chart:LineSeries.DataPointStyle>
                    <Style TargetType="{x:Type chart:LineDataPoint}">
                        <Setter Property="Background" Value="Green" />
                        <Setter Property="Height" Value="0"/>
                        <Setter Property="Width" Value="0"/>
                    </Style>
                </chart:LineSeries.DataPointStyle>
            </chart:LineSeries>

            <chart:Chart.Axes>
                <chart:DateTimeAxis Name="SharedXAxis"
                                   Orientation="X"
                                  Title="shared X-Axis"
                                  ShowGridLines="True">

                    <!--rotate the X-Axis labels -->
                    <chart:DateTimeAxis.AxisLabelStyle>
                        <Style TargetType="chart:DateTimeAxisLabel">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="chart:DateTimeAxisLabel">
                                        <TextBlock Text="{TemplateBinding FormattedContent}">
                                            <TextBlock.LayoutTransform>
                                                <RotateTransform Angle="90" CenterX = "40" CenterY = "30"/>
                                            </TextBlock.LayoutTransform>
                                        </TextBlock>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </chart:DateTimeAxis.AxisLabelStyle>

                </chart:DateTimeAxis>
            </chart:Chart.Axes>
        </chart:Chart>

    </Grid>
</Window>

 

using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.DataVisualization.Charting;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;

namespace Demo {
    public partial class MainWindow : Window {

        private Model _Model;
        private bool _DrawNewLine = false;

        public MainWindow() {
            InitializeComponent();
        } // constructor

        private void Window_Loaded(object sender, RoutedEventArgs e) {
            ViewModel lViewModel = new ViewModel(myChart);
            DataContext = lViewModel;
            _Model = new Model(lViewModel);
        } // 

        private void OnMouseMove(object sender, MouseEventArgs e) {
            if ((!_DrawNewLine) && (e.LeftButton == MouseButtonState.Pressed)) {
                MouseButtonEventArgs lMouseButtonEventArgs = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, MouseButton.Left);
                OnMouseLeftButtonDown(sender, lMouseButtonEventArgs);
                return;
            }

            IInputElement lInputElement = sender as IInputElement; // == basically Chart or LineSeries
            Point lPoint = e.GetPosition(lInputElement);

            Chart lChart = sender as Chart;
            if (lChart != null) {
                string s = string.Empty;

                // iterate through all axes
                lock (lChart.ActualAxes) {
                    foreach (IAxis lAxis in lChart.ActualAxes) {
                        RangeAxis lRangeAxis = lAxis as RangeAxis;
                        if (lRangeAxis == null) continue; // won't happen
                        AxisPoint lAxisPoint = AxisPointFactory.getAxisPoint(lChart, lRangeAxis, lPoint);

                        s += lAxisPoint.ToString() + Environment.NewLine;
                    }
                }

                InfoBox.Text = s;
                return;
            }

            LineSeries lLineSeries = sender as LineSeries;
            if (lLineSeries != null) {
                IInputElement lSelection = lLineSeries.InputHitTest(lPoint);
                if (lSelection == null) return;
                InfoBox.Text = "LineSeries object: " + lSelection.GetType().ToString();
                return;
            }
        } //

        private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
            Chart lChart = sender as Chart;
            if (lChart == null) return;

            AxisPoint lAxisPointX;
            AxisPoint lAxisPointY;
            LineSeries lLineSeries = lChart.Series[0] as LineSeries;
            if (lLineSeries == null) return;   // won't happen anyway
            if (!getAxisPointsToDrawALine(sender, e, lLineSeries, out lAxisPointX, out lAxisPointY)) return;

            AxisPointDateTime lAxisPointDateTimeX = lAxisPointX as AxisPointDateTime;
            AxisPointLinear lAxisPointLinearY = lAxisPointY as AxisPointLinear;

            if (lAxisPointDateTimeX == null) return;  // Quick and dirty test. Should not happen anyway.
            if (lAxisPointLinearY == null) return;  // Might be helpful when you change the code and forget about this requirement in another scenario.

            DrawCurve_WPF_Runtime(lChart, lAxisPointDateTimeX, lAxisPointLinearY);
        } //

        // add a new curve and its first point
        private ObservableCollection<PriceClusterSimple> ManualPoints = null;
        private void DrawCurve_WPF_Runtime(Chart lChart, AxisPointDateTime lAxisPointDateTimeX, AxisPointLinear lAxisPointLinearY) {
            PriceClusterSimple lDataPoint = new PriceClusterSimple(lAxisPointDateTimeX.MouseAxisValueAbsolute, lAxisPointLinearY.MouseAxisValueAbsolute);

            if (ManualPoints != null) {
                if (_DrawNewLine) {
                    _DrawNewLine = false;
                    ManualPoints.Clear();
                    ManualPoints.Add(lDataPoint); // from
                    ManualPoints.Add(lDataPoint); // to
                }
                else {
                    ManualPoints.RemoveAt(1);
                    ManualPoints.Add(lDataPoint); // to
                }
                return;
            }

            ManualPoints = new ObservableCollection<PriceClusterSimple>();
            ManualPoints.Add(lDataPoint);
            ManualPoints.Add(lDataPoint);
            LineSeries lNewLineSeries = new LineSeries();
            lNewLineSeries.Title = "manually added curve";
            lNewLineSeries.SetBinding(LineSeries.ItemsSourceProperty, new Binding());
            lNewLineSeries.ItemsSource = ManualPoints;
            lNewLineSeries.IndependentValueBinding = new Binding("Date");
            lNewLineSeries.DependentValueBinding = new Binding("Price");
            lNewLineSeries.DependentRangeAxis = lAxisPointLinearY.Axis;
            Setter lHeightSetter = new Setter(FrameworkElement.HeightProperty, 0.0);
            Setter lWidthSetter = new Setter(FrameworkElement.WidthProperty, 0.0);
            Setter lColor = new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Black));
            Style lStyle = new Style(typeof(Control));
            lNewLineSeries.DataPointStyle = lStyle;
            lStyle.Setters.Add(lHeightSetter);
            lStyle.Setters.Add(lWidthSetter);
            lStyle.Setters.Add(lColor);
            lChart.Series.Add(lNewLineSeries);
        } //

        private bool getAxisPointsToDrawALine(object xSender, MouseButtonEventArgs xMouseButtonEventArgs, LineSeries xLineSeries, out AxisPoint xAxisPointX, out AxisPoint xAxisPointY) {
            IInputElement lInputElement = xSender as IInputElement; // == basically Chart or LineSeries
            Point lPoint = xMouseButtonEventArgs.GetPosition(lInputElement);
            xAxisPointX = null;
            xAxisPointY = null;

            Chart lChart = xSender as Chart;
            if (lChart == null) return false;

            RangeAxis lRangeAxis;
            AxisPoint lAxisPoint;

            lRangeAxis = xLineSeries.ActualDependentRangeAxis as RangeAxis;
            if (lRangeAxis == null) return false; // won't happen in our example
            lAxisPoint = AxisPointFactory.getAxisPoint(lChart, lRangeAxis, lPoint);
            if (lRangeAxis.Orientation == AxisOrientation.X) xAxisPointX = lAxisPoint;
            else xAxisPointY = lAxisPoint;

            lRangeAxis = xLineSeries.ActualIndependentAxis as RangeAxis;
            if (lRangeAxis == null) return false; // won't happen in our example
            lAxisPoint = AxisPointFactory.getAxisPoint(lChart, lRangeAxis, lPoint);
            if (lRangeAxis.Orientation == AxisOrientation.X) xAxisPointX = lAxisPoint;
            else xAxisPointY = lAxisPoint;

            if ((xAxisPointX == null) || (xAxisPointY == null)) return false;
            return true;
        } //

        private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) {
            _DrawNewLine = true;
        } //

    } // class
} // namespace

 

using System;
using System.Linq;
using System.Windows.Threading;

namespace Demo {
	public class Model {
		private ViewModel _ViewModel;

		public Model(ViewModel xViewModel) {
			_ViewModel = xViewModel;
			DispatcherTimer lTimer = new DispatcherTimer();
			lTimer.Interval = new TimeSpan(0, 0, 3);
			lTimer.Tick += new EventHandler(Timer_Tick);
			lTimer.Start();
		} // constructor

		void Timer_Tick(object sender, EventArgs e) {
			Random r = new Random();
			PriceCluster lPriceCluster = _ViewModel.Points.Last();
			double lVW = lPriceCluster.PriceVW * (1 + ((2.0 * (r.NextDouble() - 0.5)) / 30.0));
			double lDaimler = lPriceCluster.PriceDaimler * (1 + ((2.0 * (r.NextDouble() - 0.5)) / 30.0));
			_ViewModel.AddPoint(lPriceCluster.Date.AddDays(1), lVW, lDaimler);
		} //

	} // class
} // namespace

 

using System;

namespace Demo {

    public class PriceCluster {
		public DateTime Date { get; set; }
		public double PriceVW { get; set; }
		public double PriceDaimler { get; set; }

		public PriceCluster(DateTime xDate, double xPriceVW, double xPriceDaimler) {
			Date = xDate;
			PriceVW = xPriceVW;
			PriceDaimler = xPriceDaimler;
		} // constructor
	} // class

    public class PriceClusterSimple {
        public DateTime Date { get; set; }
        public double Price { get; set; }

        public PriceClusterSimple(DateTime xDate, double xPrice) {
            Date = xDate;
            Price = xPrice;
        } // constructor
    } // class

} // namespace

 

using System;
using System.Collections.ObjectModel;
using System.Windows.Controls.DataVisualization.Charting;

namespace Demo {
	public class ViewModel {
		private readonly Chart _Chart;
		public ReadOnlyObservableCollection<PriceCluster> Points { get; private set; }
		private ObservableCollection<PriceCluster> _Points = new ObservableCollection<PriceCluster>();

		public ViewModel(Chart xChart) {
			_Chart = xChart;

			AddPoint(new DateTime(2014, 04, 10), 67.29, 13.85);
			AddPoint(new DateTime(2014, 04, 11), 66.15, 13.66);
			AddPoint(new DateTime(2014, 04, 14), 66.22, 13.67);
			AddPoint(new DateTime(2014, 04, 15), 63.99, 13.49);
			AddPoint(new DateTime(2014, 04, 16), 65.32, 13.62);
			AddPoint(new DateTime(2014, 04, 17), 67.29, 13.73);
			AddPoint(new DateTime(2014, 04, 22), 68.72, 13.91);
			AddPoint(new DateTime(2014, 04, 23), 67.85, 13.84);
			AddPoint(new DateTime(2014, 04, 24), 67.75, 13.78);
			AddPoint(new DateTime(2014, 04, 25), 66.29, 13.60);
			AddPoint(new DateTime(2014, 04, 28), 66.99, 13.73);
			AddPoint(new DateTime(2014, 04, 29), 67.79, 13.91);
			AddPoint(new DateTime(2014, 04, 30), 66.73, 13.79);
			AddPoint(new DateTime(2014, 05, 02), 66.24, 13.10);
			AddPoint(new DateTime(2014, 05, 05), 65.90, 13.08);
			AddPoint(new DateTime(2014, 05, 06), 65.16, 13.04);
			AddPoint(new DateTime(2014, 05, 07), 64.80, 13.18);
			AddPoint(new DateTime(2014, 05, 08), 65.00, 13.45);
			AddPoint(new DateTime(2014, 05, 09), 64.52, 13.42);
			AddPoint(new DateTime(2014, 05, 12), 65.28, 13.58);
			AddPoint(new DateTime(2014, 05, 13), 66.48, 13.40);
			AddPoint(new DateTime(2014, 05, 14), 66.74, 13.26);
			AddPoint(new DateTime(2014, 05, 15), 66.00, 12.97);
			AddPoint(new DateTime(2014, 05, 16), 65.21, 13.08);
			AddPoint(new DateTime(2014, 05, 19), 66.02, 13.38);
			AddPoint(new DateTime(2014, 05, 20), 66.46, 13.42);
			AddPoint(new DateTime(2014, 05, 21), 67.15, 13.84);
			AddPoint(new DateTime(2014, 05, 22), 67.52, 13.84);
			AddPoint(new DateTime(2014, 05, 23), 68.14, 14.06);
			AddPoint(new DateTime(2014, 05, 26), 69.61, 14.17);
			AddPoint(new DateTime(2014, 05, 27), 69.56, 14.15);
			AddPoint(new DateTime(2014, 05, 28), 69.29, 14.17);
			AddPoint(new DateTime(2014, 05, 29), 69.65, 14.18);
			AddPoint(new DateTime(2014, 05, 30), 69.70, 14.29);
			AddPoint(new DateTime(2014, 06, 02), 69.32, 14.31);
			AddPoint(new DateTime(2014, 06, 03), 69.68, 14.32);
			AddPoint(new DateTime(2014, 06, 04), 69.31, 14.31);
			AddPoint(new DateTime(2014, 06, 05), 70.31, 14.34);
			AddPoint(new DateTime(2014, 06, 06), 70.24, 14.42);
			AddPoint(new DateTime(2014, 06, 09), 70.09, 14.42);
			AddPoint(new DateTime(2014, 06, 10), 70.08, 14.47);
			AddPoint(new DateTime(2014, 06, 11), 69.66, 14.30);
			AddPoint(new DateTime(2014, 06, 12), 69.49, 14.26);
			AddPoint(new DateTime(2014, 06, 13), 69.12, 14.42);
			AddPoint(new DateTime(2014, 06, 16), 69.05, 14.44);
			AddPoint(new DateTime(2014, 06, 17), 69.65, 14.43);
			AddPoint(new DateTime(2014, 06, 18), 69.62, 14.62);
			AddPoint(new DateTime(2014, 06, 19), 70.10, 14.93);
			AddPoint(new DateTime(2014, 06, 20), 70.08, 14.93);
			AddPoint(new DateTime(2014, 06, 23), 69.46, 14.97);
			AddPoint(new DateTime(2014, 06, 24), 69.04, 15.06);
			AddPoint(new DateTime(2014, 06, 25), 68.71, 14.89);
			AddPoint(new DateTime(2014, 06, 26), 68.14, 15.12);
			AddPoint(new DateTime(2014, 06, 27), 68.33, 15.17);
			AddPoint(new DateTime(2014, 06, 30), 68.40, 15.08);
			AddPoint(new DateTime(2014, 07, 01), 69.19, 15.21);
			AddPoint(new DateTime(2014, 07, 02), 69.72, 15.20);
			AddPoint(new DateTime(2014, 07, 03), 70.44, 15.31);
			AddPoint(new DateTime(2014, 07, 04), 70.44, 15.16);
			AddPoint(new DateTime(2014, 07, 07), 69.28, 14.95);
			AddPoint(new DateTime(2014, 07, 08), 68.15, 14.84);
			AddPoint(new DateTime(2014, 07, 09), 68.16, 14.73);
			AddPoint(new DateTime(2014, 07, 10), 67.05, 14.43);
			AddPoint(new DateTime(2014, 07, 11), 66.68, 14.50);
			AddPoint(new DateTime(2014, 07, 14), 67.61, 14.60);
			AddPoint(new DateTime(2014, 07, 15), 67.28, 14.70);
			AddPoint(new DateTime(2014, 07, 16), 67.77, 14.89);
			AddPoint(new DateTime(2014, 07, 17), 66.56, 14.53);
			AddPoint(new DateTime(2014, 07, 18), 65.40, 14.52);
			AddPoint(new DateTime(2014, 07, 21), 64.84, 14.49);
			AddPoint(new DateTime(2014, 07, 22), 66.09, 14.83);
			AddPoint(new DateTime(2014, 07, 23), 65.58, 14.74);
			AddPoint(new DateTime(2014, 07, 24), 66.30, 14.92);
			AddPoint(new DateTime(2014, 07, 25), 65.15, 14.65);
			AddPoint(new DateTime(2014, 07, 28), 63.08, 14.61);
			AddPoint(new DateTime(2014, 07, 29), 63.89, 14.71);
			AddPoint(new DateTime(2014, 07, 30), 63.07, 14.43);
			AddPoint(new DateTime(2014, 07, 31), 61.88, 14.13);
			AddPoint(new DateTime(2014, 08, 01), 60.85, 13.60);
			AddPoint(new DateTime(2014, 08, 04), 61.17, 13.58);
			AddPoint(new DateTime(2014, 08, 05), 60.43, 13.61);
			AddPoint(new DateTime(2014, 08, 06), 59.82, 13.40);
			AddPoint(new DateTime(2014, 08, 07), 58.95, 13.16);
			AddPoint(new DateTime(2014, 08, 08), 59.27, 13.16);
			AddPoint(new DateTime(2014, 08, 11), 60.71, 13.36);
			AddPoint(new DateTime(2014, 08, 12), 59.85, 13.17);
			AddPoint(new DateTime(2014, 08, 13), 60.66, 13.80);
			AddPoint(new DateTime(2014, 08, 14), 61.07, 13.77);
			AddPoint(new DateTime(2014, 08, 15), 59.71, 13.65);
			AddPoint(new DateTime(2014, 08, 18), 60.99, 13.72);
			AddPoint(new DateTime(2014, 08, 19), 61.60, 13.72);
			AddPoint(new DateTime(2014, 08, 20), 61.33, 13.82);
			AddPoint(new DateTime(2014, 08, 21), 62.20, 13.86);
			AddPoint(new DateTime(2014, 08, 22), 61.65, 13.70);
			AddPoint(new DateTime(2014, 08, 25), 62.88, 13.88);
			AddPoint(new DateTime(2014, 08, 26), 63.49, 13.87);
			AddPoint(new DateTime(2014, 08, 27), 63.15, 13.89);
			AddPoint(new DateTime(2014, 08, 28), 62.16, 13.77);
			AddPoint(new DateTime(2014, 08, 29), 62.24, 13.83);
			AddPoint(new DateTime(2014, 09, 01), 61.88, 13.92);
			AddPoint(new DateTime(2014, 09, 02), 61.82, 13.92);
			AddPoint(new DateTime(2014, 09, 03), 62.90, 14.17);
			AddPoint(new DateTime(2014, 09, 04), 64.14, 14.34);
			AddPoint(new DateTime(2014, 09, 05), 65.17, 14.40);

			Points = new ReadOnlyObservableCollection<PriceCluster>(_Points);
		} // constructor

		// only to be called from the dispatcher thread!
		public void AddPoint(DateTime xDate, double xPriceVW, double xPriceDaimler) {
			_Points.Add(new PriceCluster(xDate, xPriceVW, xPriceDaimler));
		} //

	} // class
} // namespace

 

Timer Finetuning, Each Microsecond Counts

Einstein

 

This is a one day excursus from my present chart posts, which will continue with my next post.

Exact timing is not an issue for most of us. Who cares if something gets executed a few millisecond earlier or later. Well, this blog was started to highlight many issues of “hard-core” programming. Thus I do care!

Back in 1985, when I was hacking on my Commodore 64, there was no such as Context Switching. To wait for a few moments you could run a loop for a number of times. And when you were running the same loop on the same C64 model again, then you had pretty much the same passed amount of time. The emphasis in on the word “same”. But these days don’t work like that. You never face the “same” situation again. Since the invention of multithreading (in fact it was called multitasking on the good old Commodore Amiga) context switching came into place.

(There are Multimedia Timers in the windows core environment. I am not going to use these. High precision inside the standard .Net framework is possible. There is no need to leave robustness behind.)

I was running several small tests to get a feeling for the .Net timing. The first and most simple approach was using PriorityBoostEnabled on the process to boost the process priority. Unfortunately its use is limited. Your priority only gets boosted, when your main window has the focus. And when your process is on a high priority anyway, it obviously won’t change a lot.
Therefore the next step was to increase the process priority to ‘ProcessPriorityClass.RealTime’. This is a dangerous setting and should only be used by skilled programmers. Your application could consume too much privileged time at the expense of the entire system, which would result in a slowed down execution time for ALL processes and services – including your process that you were originally trying to speed-up.

One important aspect is the PrivilegedProcessorTime. We’ll assume 80 milliseconds, just to have a realistic number. This would imply that the windows system was giving your process 80 ms to execute code before it switched to another process. You have to share this order of magnitude amongst all your threads. 10 ms of these are consumed by your GUI update. Make sure to execute any time sensitive code in one piece before the next context switching takes place.

As we talk about timers, it does not help you at all to be precise on one side, but then lose processor execution time to another thread or process.
Let me come up with a short story. You take the train at exactly 1 pm after waiting for 8 hours. You only have 10 minutes left to get from Austria to Belgium. That sucks, right? You obviously have to start your journey without waiting 8 hours beforehand.
The hard-core scenario would be to get changed, pack your suitcase and sleep until 12:45pm … yes, in your business suit! Then suddenly wake up, jump out of your bed, don’t kiss your wife, run to the station and use the full 8 hours for your journey. You only make it, when you arrive in Brussels before the ticket collector kicks you out, because your ticket was only valid for 8 hours.

 

Let’s delve into practice. In the below example you can see that getting the time-stamp does take about half a tick. When you run a long loop and try to get the time-stamp many times, then it seems to slow down. The reason for this is the context switching. Somewhere in the middle the PrivilegedProcessorTime expired. Therefore running only a few loops can show better results.

The system timer is terribly slow. Its delays and reliability are bad as bad can be. It apparently accepts a double for microseconds rather than the usual long. You would expect a higher resolution then, wouldn’t you?

Especially the first run is unpredictable. The reason for this are the CLR runtime compilation operations. Your code gets compiled shortly before the first execution. You will observe this with any timer. Start your code earlier and skip the first callback. This improves the precision for the first callback you are really waiting for.

The thread timer is more precise, but is firing too early sometimes. Its precision is above one millisecond.

The timer with the most potential isn’t really a timer. It is a thread sleep method in a loop. In my opinion that is the most advanced solution to solve the timer precision problem. The loop is running on a thread. This thread is not shared like on a task scheduler. You own that thread and nobody else will use it. You obviously should not run hundreds of threads. Run 5 of them, and you are still in the green zone. The big advantage is that you can change the priority of this thread to ‘Highest’. This gives you the attention that real geeks need. Furthermore, you won’t have any multiple code executions. There is one event running at a time. If you miss one, because the previous event was running too long, then you can still decide to not execute the next run. A general system delay would then not queue up events that you don’t want to execute anymore … as it is too late anyway. You obviously can add this feature for any timer, but this one is the safest way to easily have only one event running at a time.

Check out my MicroTimer class. It waits a little bit less than required and then calls SpinWait multiple times. Once the process thread gets processor time assigned, you most likely won’t face context switching. You wait like a disciple for the one and only messiah, running around a rigid pole.

The MicroTimer class should give you a whopping precision of 1 microsecond …. unless my usual ‘unless’ sentence states something else ;)
The example output shows the delay at the point when the timer was leaving the inner loop to raise an event. And it shows the delay at the time it recorded the data inside the event. Obviously there are some microseconds in between. Measure that time and adjust your schedule accordingly.

 

Computer Specs:
Lenovo Yoga 2 11”
Intel i5-4202Y 1.6 GHz processor
128GB SSD hard disk
4GB memory
Windows 8.1, 64 bit

 

Admittedly, the code is a bit of Spaghetti style today. Tests are really what they are. It was clear that the MicroTimer would win in the end. So my effort for a proper style went into that direction.

 

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace TimerPrecisionTest {
	class Program {
		private const long _NumDataPoints = 10;
		private static List<long> _DataPoints = new List<long>();
		private static int _CurrentDataPoint = 0;

		private static long[] _Schedule;

		private static long _PeriodInMs = 3 * Stopwatch.Frequency * 1000;
		private static long _PeriodInTicks = 3 * Stopwatch.Frequency;
		private static double _FrequencyAsDouble = (double)Stopwatch.Frequency;

		private static AutoResetEvent _AutoResetEvent = new AutoResetEvent(false);

		static void Main(string[] args) {
			Process p = Process.GetCurrentProcess();
			p.PriorityBoostEnabled = true;  // every little helps
			p.PriorityClass = ProcessPriorityClass.Normal;
			Console.WriteLine("Process with normal priority:");
			Console.WriteLine("Priviledged processor time for process " + p.ProcessName + " is " + p.PrivilegedProcessorTime.TotalMilliseconds.ToString("#,##0.0") + " ms");
			p.PriorityClass = ProcessPriorityClass.RealTime;
			Console.WriteLine("Process with high priority:");
			Console.WriteLine("Priviledged processor time for process " + p.ProcessName + " is " + p.PrivilegedProcessorTime.TotalMilliseconds.ToString("#,##0.0") + " ms");

			Console.WriteLine("IsHighResolution system clock: " + Stopwatch.IsHighResolution);
			Console.WriteLine("Number of ticks per second: " + Stopwatch.Frequency.ToString("#,##0"));

			long a = Stopwatch.GetTimestamp();
			for (int i = 0; i < 100000; i++) {
				long b = Stopwatch.GetTimestamp();
			}
			long c = Stopwatch.GetTimestamp();
			Console.WriteLine("Number of ticks to obtain a timestamp: " + ((c - a) / 100000.0).ToString("#,##0.00"));
			Console.WriteLine();

			UseSystemTimer();

			UseThreadingTimer();

			// a simple loop
			Thread lThread = new Thread(new ThreadStart(UseLoop));
			lThread.Priority = ThreadPriority.Highest;
			lThread.IsBackground = true;
			lThread.Start();

			_AutoResetEvent.WaitOne();

			testSpinWaitPrecision();

			// a proper loop
			UseMicroTimerClass();

			Console.ReadLine();
		} //

	#region MicroTimer
		private static void UseMicroTimerClass() {
			Console.WriteLine("MICRO TIMER CLASS:");
			Init();
			long lMaxDelay = (3L * Stopwatch.Frequency) / 1000L; // 3 ms
			MicroTimer lMicroTimer = new MicroTimer(new Queue<long>(_Schedule), lMaxDelay);
			lMicroTimer.OnMicroTimer += OnMicroTimer;
			lMicroTimer.OnMicroTimerStop += OnMicroTimerStop;
			lMicroTimer.OnMicroTimerSkipped += OnMicroTimerSkipped;
			lMicroTimer.Start();
		} //

		static void OnMicroTimerSkipped(int xSenderThreadId, long xWakeUpTimeInTicks, long xDelayInTicks) {
			Console.WriteLine("MicroTimer for WakeUpTime " + xWakeUpTimeInTicks + " did not run. Delay was: " + xDelayInTicks);
		} //

		static void OnMicroTimerStop(int xSenderThreadId) {
			Console.WriteLine("MicroTimer stopped.");
			PrintStats();
		} //

		static void OnMicroTimer(int xSenderThreadId, long xWakeUpTimeInTicks, long xDelayInTicks) {
			RecordDatapoint();
			Console.WriteLine("(Delay at wakeup time was " + xDelayInTicks.ToString("#,##0" + " tick)"));
		} //
	#endregion

		#region SpinWait precision
		private static void testSpinWaitPrecision() {
			Console.WriteLine();
			Console.WriteLine("SpinWait tests (neglecting PrivilegedProcessorTime):");
			Thread.CurrentThread.Priority = ThreadPriority.Highest;
			Thread.Sleep(0);  // switch context at a good point
			long a = Stopwatch.GetTimestamp();
			Thread.SpinWait(100000);
			long b = Stopwatch.GetTimestamp();
			Console.WriteLine("Number of ticks for a SpinWait: " + (b - a).ToString("#,##0"));

			a = Stopwatch.GetTimestamp();
			Thread.Sleep(0);
			for (int i = 0; i < 100; i++) {
				Thread.SpinWait(100000);
			}
			b = Stopwatch.GetTimestamp();
			double lAverage = (b - a) / 100.0;
			Console.WriteLine("Average ticks for 100x SpinWaits: " + lAverage.ToString("#,##0") + " == " + (lAverage * 1000.0 * 1000.0/ Stopwatch.Frequency).ToString("#,##0.0000") + " microseconds");

		  // now we do get extremly precise
			long lEndTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency; // wake up in one second
			Thread.Sleep(900); // imagine a timer raises an event roughly 100ms too early
			while (Stopwatch.GetTimestamp() < lEndTime) {
				Thread.SpinWait(10);  // no context switching
			}
			a = Stopwatch.GetTimestamp();
			Console.WriteLine("SpinWait caused an error of just: " + ((a - lEndTime) * 1000.0 * 1000.0 / _FrequencyAsDouble).ToString("#,##0.0000") + " microseconds");
			Thread.CurrentThread.Priority = ThreadPriority.Normal;
			Console.WriteLine();
		} //
		#endregion

		#region simple loop
		private static void UseLoop() {
			Thread.CurrentThread.Priority = ThreadPriority.Highest;
			Console.WriteLine("LOOP AND SLEEP:");
			Init();
			Thread.Sleep((int)getTimeMsToNextCall_Long());

			while (_CurrentDataPoint < _NumDataPoints) {
				RecordDatapoint();
				if (_CurrentDataPoint >= _NumDataPoints) break;
				Thread.Sleep((int)getTimeMsToNextCall_Long());
			}

			PrintStats();
			_AutoResetEvent.Set();
			Thread.CurrentThread.Priority = ThreadPriority.Normal;
		} //
		#endregion

		#region SystemTimer
		private static System.Timers.Timer _SystemTimer = null;
		private static void UseSystemTimer() {
			Console.WriteLine("SYSTEM TIMER:");
			Init();
			_SystemTimer = new System.Timers.Timer();
			_SystemTimer.AutoReset = false;
			_SystemTimer.Elapsed += SystemTimer_Elapsed;
			_SystemTimer.Interval = getTimeMsToNextCall_Double();  // do not init in constructor!
			_SystemTimer.Start();

			_AutoResetEvent.WaitOne();
			_SystemTimer.Stop();
			_SystemTimer = null;

			PrintStats();
		} //

		private static void SystemTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) {
			RecordDatapoint();

			// calibrate timer (we did not start with the right interval when we launched it)
			System.Timers.Timer lTimer = _SystemTimer;
			if (lTimer == null) return;
			if (_CurrentDataPoint >= _NumDataPoints) return;
			lTimer.Stop();
			lTimer.Interval = getTimeMsToNextCall_Double();
			lTimer.Start();
		} //
		#endregion

		#region ThreadingTimer
		private static System.Threading.Timer _ThreadingTimer = null;

		private static void UseThreadingTimer() {
			Console.WriteLine("THREAD TIMER:");
			Init();

			TimerCallback lCallback = new TimerCallback(ThreadingTimer_Elapsed);
			_ThreadingTimer = new System.Threading.Timer(lCallback, null, getTimeMsToNextCall_Long(), (long)(Timeout.Infinite));

			_AutoResetEvent.WaitOne();
			_ThreadingTimer = null;

			PrintStats();
		} //

		private static void ThreadingTimer_Elapsed(object xState) {
			RecordDatapoint();

			// restart timer
			System.Threading.Timer lTimer = _ThreadingTimer;
			if (lTimer == null) return;
			if (_CurrentDataPoint >= _NumDataPoints) return;
			lTimer.Change(getTimeMsToNextCall_Long(), (long)Timeout.Infinite);
		} //
		#endregion

		#region miscellaneous
		private static void Init() {
			_DataPoints.Clear();
			_CurrentDataPoint = 0;

			// init exact time schedule
			long lOffset = Stopwatch.GetTimestamp() + _PeriodInTicks;  // we start in the future
			_Schedule = new long[_NumDataPoints];
			for (int i = 0; i < _NumDataPoints; i++) {
				_Schedule[i] = lOffset;
				lOffset += _PeriodInTicks;
			}
		} //

		private static void PrintStats() {
			if (_DataPoints.Count < 1) return;

			Console.WriteLine("Average " + _DataPoints.Average());
			long lMin = _DataPoints.Min();
			long lMax = _DataPoints.Max();
			Console.WriteLine("Min     " + lMin);
			Console.WriteLine("Max     " + lMax);
			Console.WriteLine("Range   " + (lMax - lMin));
			Console.WriteLine();
		} //

		private static void RecordDatapoint() {
			long lDifference = Stopwatch.GetTimestamp() - _Schedule[_CurrentDataPoint];  // positive = late, negative = early
			_DataPoints.Add(lDifference);
			Console.WriteLine("Delay in ticks: " + lDifference.ToString("#,##0") + " == " + ((lDifference * 1000000.0) / _FrequencyAsDouble).ToString("#,##0") + " microseconds");
			_CurrentDataPoint++;
			if (_CurrentDataPoint >= _NumDataPoints) _AutoResetEvent.Set();
		} //

		private static long getTimeMsToNextCall_Long() {
			long lTicks = (_Schedule[_CurrentDataPoint] - Stopwatch.GetTimestamp());
			return (1000 * lTicks) / Stopwatch.Frequency;
		} //

		private static double getTimeMsToNextCall_Double() {
			double lTicks = (double)(_Schedule[_CurrentDataPoint] - Stopwatch.GetTimestamp());
			return (1000.0 * lTicks) / _FrequencyAsDouble;
		} //
		#endregion

	} // class
} // namespace

 

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace TimerPrecisionTest {
	public class MicroTimer {

		private readonly Queue<long> _TickTimeTable;
		private readonly Thread _Thread;
		private readonly long _MaxDelayInTicks;  // do not run if the delay was too long
		private long _NextWakeUpTickTime;

		public delegate void dOnMicroTimer(int xSenderThreadId, long xWakeUpTimeInTicks, long xDelayInTicks);
		public event dOnMicroTimer OnMicroTimer;
		public event dOnMicroTimer OnMicroTimerSkipped;

		public delegate void dQuickNote(int xSenderThreadId);
		public event dQuickNote OnMicroTimerStart;
		public event dQuickNote OnMicroTimerStop;

		public MicroTimer(Queue<long> xTickTimeTable, long xMaxDelayInTicks) {
			_TickTimeTable = xTickTimeTable;
			_Thread = new Thread(new ThreadStart(Loop));
			_Thread.Priority = ThreadPriority.Highest;
			_Thread.Name = "TimerLoop";
			_Thread.IsBackground = true;
			_MaxDelayInTicks = xMaxDelayInTicks;
		} //

		public int Start() {
			if ((_Thread.ThreadState & System.Threading.ThreadState.Unstarted) == 0) return -1;
			_Thread.Start();
			return _Thread.ManagedThreadId;
		} //

		public void Stop() {
			_Thread.Interrupt();
		} //

		private void Loop() {
			dQuickNote lOnStart = OnMicroTimerStart;
			if (lOnStart != null) lOnStart(_Thread.ManagedThreadId);

			try {
				while (true) {
					if (_TickTimeTable.Count < 1) break;
					_NextWakeUpTickTime = _TickTimeTable.Dequeue();
					long lMilliseconds = _NextWakeUpTickTime - Stopwatch.GetTimestamp();
					if (lMilliseconds < 0L) continue;
					lMilliseconds = (lMilliseconds * 1000) / Stopwatch.Frequency;
					lMilliseconds -= 50;  // we want to wake up earlier and spend the last time using SpinWait
					Thread.Sleep((int)lMilliseconds);

					while (Stopwatch.GetTimestamp() < _NextWakeUpTickTime) {
						Thread.SpinWait(10);
					}
					long lWakeUpTimeInTicks = Stopwatch.GetTimestamp();
					long lDelay = lWakeUpTimeInTicks - _NextWakeUpTickTime;
					if (lDelay < _MaxDelayInTicks) {
						dOnMicroTimer lHandler = OnMicroTimer;
						if (lHandler == null) continue;
						lHandler(_Thread.ManagedThreadId, lWakeUpTimeInTicks, lDelay);
					}
					else {
						dOnMicroTimer lHandler = OnMicroTimerSkipped;
						if (lHandler == null) continue;
						lHandler(_Thread.ManagedThreadId, lWakeUpTimeInTicks, lDelay);
					}
				}
			}
			catch (ThreadInterruptedException) { }
			catch (Exception) { Console.WriteLine("Exiting timer thread."); }

			dQuickNote lOnStop = OnMicroTimerStop;
			if (lOnStop != null) lOnStop(_Thread.ManagedThreadId);
		} //

	} // class
} // namespace

Example output:

Process with normal priority:
Priviledged processor time for process TimerPrecisionTest.vshost is 109.4 ms
Process with high priority:
Priviledged processor time for process TimerPrecisionTest.vshost is 109.4 ms
IsHighResolution system clock: True
Number of ticks per second: 1,558,893
Number of ticks to obtain a timestamp: 0.27

SYSTEM TIMER:
Delay in ticks: 65,625 == 42,097 microseconds
Delay in ticks: 1,414 == 907 microseconds
Delay in ticks: 1,663 == 1,067 microseconds
Delay in ticks: 1,437 == 922 microseconds
Delay in ticks: 25,829 == 16,569 microseconds
Delay in ticks: 1,532 == 983 microseconds
Delay in ticks: 14,478 == 9,287 microseconds
Delay in ticks: 14,587 == 9,357 microseconds
Delay in ticks: 14,615 == 9,375 microseconds
Delay in ticks: 14,650 == 9,398 microseconds
Average 15583
Min 1414
Max 65625
Range 64211

THREAD TIMER:
Delay in ticks: 18,890 == 12,118 microseconds
Delay in ticks: 17,493 == 11,221 microseconds
Delay in ticks: 11,750 == 7,537 microseconds
Delay in ticks: 11,824 == 7,585 microseconds
Delay in ticks: 11,914 == 7,643 microseconds
Delay in ticks: 11,858 == 7,607 microseconds
Delay in ticks: 11,935 == 7,656 microseconds
Delay in ticks: 12,049 == 7,729 microseconds
Delay in ticks: 12,108 == 7,767 microseconds
Delay in ticks: 24,953 == 16,007 microseconds
Average 14477.4
Min 11750
Max 24953
Range 13203

LOOP AND SLEEP:
Delay in ticks: 7,346 == 4,712 microseconds
Delay in ticks: 7,367 == 4,726 microseconds
Delay in ticks: 7,423 == 4,762 microseconds
Delay in ticks: 7,494 == 4,807 microseconds
Delay in ticks: 7,542 == 4,838 microseconds
Delay in ticks: 7,408 == 4,752 microseconds
Delay in ticks: 20,249 == 12,989 microseconds
Delay in ticks: 20,275 == 13,006 microseconds
Delay in ticks: 20,351 == 13,055 microseconds
Delay in ticks: 20,383 == 13,075 microseconds
Average 12583.8
Min 7346
Max 20383
Range 13037

SpinWait tests (neglecting PrivilegedProcessorTime):

Number of ticks for a SpinWait: 4,833
Average ticks for 100x SpinWaits: 1,422 == 912.0831 microseconds
SpinWait caused an error of just: 0.0000 microseconds

MICRO TIMER CLASS:
Delay in ticks: 772 == 495 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 34 == 22 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 5 == 3 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 5 == 3 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 7 == 4 microseconds
(Delay at wakeup time was 2 tick)
MicroTimer stopped.
Average 85.3
Min 5
Max 772
Range 767

WPF Charts (Part 2)

Chart2

I have been quite busy in the last weeks, I will resume blogging on a regular basis again.

Today, we are enhancing the previous chart Part1 with some new features:

  • One curve has circles around all points. These become transparent when you hover over them.
    I chose a pretty large size to make them more obvious.
  • The shared X-Axis text labels are rotated by 90 degrees. Feel free to test other angles like 45 degrees.
  • A three second interval timer appends new points to the chart.
  • A text field shows the object types while the mouse hovers over the chart elements. This is the entry point to examine chart objects, find positions and generate on the fly ToolTips.

Some code was commented out. Play with these code pieces. You can change the following behavior:

  • Hide the legend by setting its width to zero.
  • Add a non-shard Axis.
  • Change the color to blue rather transparent when hovering over a point.
  • Besides the above, play with the code as much as you like – especially the XAML part.

In this example I used the .NET ReadOnlyObservableCollection. This collection is a wrapper around the well known ObservableCollection. The readonly collection cannot be changed (if there was any chart element to eg. cut a point out). But you can access the wrapped read/write ObservableCollection. This is a nice approach. For the outer world you have an encapsulated object. From behind the curtain you still have full access. The chart automatically updates when a new point is added to the read/write ObservableCollection.

Once again the code is pretty much self explanatory. So I am not adding a long post text.

 

<Window x:Class="Demo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Demo"
    xmlns:datavisualization="clr-namespace:System.Windows.Controls.DataVisualization;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    xmlns:chart="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"
    Title="Demo Window"
    Loaded ="Window_Loaded">

    <Grid>
        <TextBox Name="InfoBox" Text="" Height="Auto"/>
        <chart:Chart Name="myChart" Title="2014" Width="Auto" Height="Auto" MinWidth="400" MinHeight="300" MouseMove="OnMouseMove">
            <!-- hide the legend -->
            <!--<chart:Chart.LegendStyle>
                <Style TargetType="datavisualization:Legend">
                    <Setter Property="Width" Value="0" />
                </Style>
            </chart:Chart.LegendStyle>-->

            <chart:LineSeries Title="Volkswagen"
                              ItemsSource="{Binding Points}"
                              IndependentValueBinding="{Binding Date}"
                              DependentValueBinding="{Binding PriceVW}"
                              MouseMove="OnMouseMove">
                <chart:LineSeries.DependentRangeAxis>
                    <chart:LinearAxis Orientation="Y" Title="Y-Axis Volkswagen" ShowGridLines="True" />
                </chart:LineSeries.DependentRangeAxis>

                <chart:LineSeries.DataPointStyle>
                    <Style TargetType="{x:Type chart:LineDataPoint}">
                        <Setter Property="Background" Value="Red" />
                        <Setter Property="Width" Value="20" />
                        <Setter Property="Height" Value="20" />

                        <Style.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <!--<Setter Property="Background" Value="Blue"/>-->
                                <Setter Property="Background" Value="Transparent" />
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </chart:LineSeries.DataPointStyle>
            </chart:LineSeries>

            <chart:LineSeries Title="Daimler"
                              ItemsSource="{Binding Points}"
                              IndependentValueBinding="{Binding Date}"
                              DependentValueBinding="{Binding PriceDaimler}">
                <chart:LineSeries.DependentRangeAxis>
                    <chart:LinearAxis Orientation="Y" Title="Y-Axis Daimler" />
                </chart:LineSeries.DependentRangeAxis>
                <!--<chart:LineSeries.IndependentAxis >
                    <chart:DateTimeAxis Orientation="X" Title="non-shared axis" />
                </chart:LineSeries.IndependentAxis>-->
                <chart:LineSeries.DataPointStyle>
                    <Style TargetType="{x:Type chart:LineDataPoint}">
                        <Setter Property="Background" Value="Green"/>
                        <Setter Property="Height" Value="0"/>
                        <Setter Property="Width" Value="0"/>
                    </Style>
                </chart:LineSeries.DataPointStyle>
            </chart:LineSeries>

            <chart:Chart.Axes>
                <chart:DateTimeAxis Name="SharedXAxis"
                                   Orientation="X"
                                  Title="shared X-Axis"
                                  ShowGridLines="True">

                    <!--rotate the X-Axis labels -->
                    <chart:DateTimeAxis.AxisLabelStyle>
                        <Style TargetType="chart:DateTimeAxisLabel">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="chart:DateTimeAxisLabel">
                                        <TextBlock Text="{TemplateBinding FormattedContent}">
                                            <TextBlock.LayoutTransform>
                                                <RotateTransform Angle="90" CenterX = "40" CenterY = "30"/>
                                            </TextBlock.LayoutTransform>
                                        </TextBlock>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </chart:DateTimeAxis.AxisLabelStyle>

                </chart:DateTimeAxis>
            </chart:Chart.Axes>
        </chart:Chart>

    </Grid>
</Window>

 

using System.Windows;
using System.Windows.Controls.DataVisualization.Charting;
using System.Windows.Input;

namespace Demo {
	public partial class MainWindow : Window {

		private Model _Model;

		public MainWindow() {
			InitializeComponent();
		} // constructor

		private void Window_Loaded(object sender, RoutedEventArgs e) {
			ViewModel lViewModel = new ViewModel(myChart);
			DataContext = lViewModel;
			_Model = new Model(lViewModel);
		} // 

		private void OnMouseMove(object sender, MouseEventArgs e) {

			IInputElement lInputElement = sender as IInputElement; // == Chart, LineSeries ...
			Chart lChart = sender as Chart;
			LineSeries lLineSeries = sender as LineSeries;

			Point lPoint = e.GetPosition(lInputElement);
			if (lChart != null) {
				IInputElement lSelection = lChart.InputHitTest(lPoint);
				if (lSelection == null) return;
				InfoBox.Text = lSelection.GetType().ToString();
			}
			else if (lLineSeries != null) {
				IInputElement lSelection = lLineSeries.InputHitTest(lPoint);
				if (lSelection == null) return;
				InfoBox.Text = lSelection.GetType().ToString();
			}
		} //

	} // class
} // namespace

 

using System;
using System.Linq;
using System.Windows.Threading;

namespace Demo {
	public class Model {
		private ViewModel _ViewModel;

		public Model(ViewModel xViewModel) {
			_ViewModel = xViewModel;
			DispatcherTimer lTimer = new DispatcherTimer();
			lTimer.Interval = new TimeSpan(0, 0, 3);
			lTimer.Tick += new EventHandler(Timer_Tick);
			lTimer.Start();
		} // constructor

		void Timer_Tick(object sender, EventArgs e) {
			Random r = new Random();
			PriceCluster lPriceCluster = _ViewModel.Points.Last();
			double lVW = lPriceCluster.PriceVW * (1 + ((2.0 * (r.NextDouble() - 0.5)) / 30.0));
			double lDaimler = lPriceCluster.PriceDaimler * (1 + ((2.0 * (r.NextDouble() - 0.5)) / 30.0));
			_ViewModel.AddPoint(lPriceCluster.Date.AddDays(1), lVW, lDaimler);
		} //

	} // class
} // namespace

 

using System;
using System.Collections.ObjectModel;
using System.Windows.Controls.DataVisualization.Charting;

namespace Demo {
	public class ViewModel {
		private readonly Chart _Chart;
		public ReadOnlyObservableCollection<PriceCluster> Points { get; private set; }
		private ObservableCollection<PriceCluster> _Points = new ObservableCollection<PriceCluster>();

		public ViewModel(Chart xChart) {
			_Chart = xChart;

			AddPoint(new DateTime(2014, 04, 10), 67.29, 13.85);
			AddPoint(new DateTime(2014, 04, 11), 66.15, 13.66);
			AddPoint(new DateTime(2014, 04, 14), 66.22, 13.67);
			AddPoint(new DateTime(2014, 04, 15), 63.99, 13.49);
			AddPoint(new DateTime(2014, 04, 16), 65.32, 13.62);
			AddPoint(new DateTime(2014, 04, 17), 67.29, 13.73);
			AddPoint(new DateTime(2014, 04, 22), 68.72, 13.91);
			AddPoint(new DateTime(2014, 04, 23), 67.85, 13.84);
			AddPoint(new DateTime(2014, 04, 24), 67.75, 13.78);
			AddPoint(new DateTime(2014, 04, 25), 66.29, 13.60);
			AddPoint(new DateTime(2014, 04, 28), 66.99, 13.73);
			AddPoint(new DateTime(2014, 04, 29), 67.79, 13.91);
			AddPoint(new DateTime(2014, 04, 30), 66.73, 13.79);
			AddPoint(new DateTime(2014, 05, 02), 66.24, 13.10);
			AddPoint(new DateTime(2014, 05, 05), 65.90, 13.08);
			AddPoint(new DateTime(2014, 05, 06), 65.16, 13.04);
			AddPoint(new DateTime(2014, 05, 07), 64.80, 13.18);
			AddPoint(new DateTime(2014, 05, 08), 65.00, 13.45);
			AddPoint(new DateTime(2014, 05, 09), 64.52, 13.42);
			AddPoint(new DateTime(2014, 05, 12), 65.28, 13.58);
			AddPoint(new DateTime(2014, 05, 13), 66.48, 13.40);
			AddPoint(new DateTime(2014, 05, 14), 66.74, 13.26);
			AddPoint(new DateTime(2014, 05, 15), 66.00, 12.97);
			AddPoint(new DateTime(2014, 05, 16), 65.21, 13.08);
			AddPoint(new DateTime(2014, 05, 19), 66.02, 13.38);
			AddPoint(new DateTime(2014, 05, 20), 66.46, 13.42);
			AddPoint(new DateTime(2014, 05, 21), 67.15, 13.84);
			AddPoint(new DateTime(2014, 05, 22), 67.52, 13.84);
			AddPoint(new DateTime(2014, 05, 23), 68.14, 14.06);
			AddPoint(new DateTime(2014, 05, 26), 69.61, 14.17);
			AddPoint(new DateTime(2014, 05, 27), 69.56, 14.15);
			AddPoint(new DateTime(2014, 05, 28), 69.29, 14.17);
			AddPoint(new DateTime(2014, 05, 29), 69.65, 14.18);
			AddPoint(new DateTime(2014, 05, 30), 69.70, 14.29);
			AddPoint(new DateTime(2014, 06, 02), 69.32, 14.31);
			AddPoint(new DateTime(2014, 06, 03), 69.68, 14.32);
			AddPoint(new DateTime(2014, 06, 04), 69.31, 14.31);
			AddPoint(new DateTime(2014, 06, 05), 70.31, 14.34);
			AddPoint(new DateTime(2014, 06, 06), 70.24, 14.42);
			AddPoint(new DateTime(2014, 06, 09), 70.09, 14.42);
			AddPoint(new DateTime(2014, 06, 10), 70.08, 14.47);
			AddPoint(new DateTime(2014, 06, 11), 69.66, 14.30);
			AddPoint(new DateTime(2014, 06, 12), 69.49, 14.26);
			AddPoint(new DateTime(2014, 06, 13), 69.12, 14.42);
			AddPoint(new DateTime(2014, 06, 16), 69.05, 14.44);
			AddPoint(new DateTime(2014, 06, 17), 69.65, 14.43);
			AddPoint(new DateTime(2014, 06, 18), 69.62, 14.62);
			AddPoint(new DateTime(2014, 06, 19), 70.10, 14.93);
			AddPoint(new DateTime(2014, 06, 20), 70.08, 14.93);
			AddPoint(new DateTime(2014, 06, 23), 69.46, 14.97);
			AddPoint(new DateTime(2014, 06, 24), 69.04, 15.06);
			AddPoint(new DateTime(2014, 06, 25), 68.71, 14.89);
			AddPoint(new DateTime(2014, 06, 26), 68.14, 15.12);
			AddPoint(new DateTime(2014, 06, 27), 68.33, 15.17);
			AddPoint(new DateTime(2014, 06, 30), 68.40, 15.08);
			AddPoint(new DateTime(2014, 07, 01), 69.19, 15.21);
			AddPoint(new DateTime(2014, 07, 02), 69.72, 15.20);
			AddPoint(new DateTime(2014, 07, 03), 70.44, 15.31);
			AddPoint(new DateTime(2014, 07, 04), 70.44, 15.16);
			AddPoint(new DateTime(2014, 07, 07), 69.28, 14.95);
			AddPoint(new DateTime(2014, 07, 08), 68.15, 14.84);
			AddPoint(new DateTime(2014, 07, 09), 68.16, 14.73);
			AddPoint(new DateTime(2014, 07, 10), 67.05, 14.43);
			AddPoint(new DateTime(2014, 07, 11), 66.68, 14.50);
			AddPoint(new DateTime(2014, 07, 14), 67.61, 14.60);
			AddPoint(new DateTime(2014, 07, 15), 67.28, 14.70);
			AddPoint(new DateTime(2014, 07, 16), 67.77, 14.89);
			AddPoint(new DateTime(2014, 07, 17), 66.56, 14.53);
			AddPoint(new DateTime(2014, 07, 18), 65.40, 14.52);
			AddPoint(new DateTime(2014, 07, 21), 64.84, 14.49);
			AddPoint(new DateTime(2014, 07, 22), 66.09, 14.83);
			AddPoint(new DateTime(2014, 07, 23), 65.58, 14.74);
			AddPoint(new DateTime(2014, 07, 24), 66.30, 14.92);
			AddPoint(new DateTime(2014, 07, 25), 65.15, 14.65);
			AddPoint(new DateTime(2014, 07, 28), 63.08, 14.61);
			AddPoint(new DateTime(2014, 07, 29), 63.89, 14.71);
			AddPoint(new DateTime(2014, 07, 30), 63.07, 14.43);
			AddPoint(new DateTime(2014, 07, 31), 61.88, 14.13);
			AddPoint(new DateTime(2014, 08, 01), 60.85, 13.60);
			AddPoint(new DateTime(2014, 08, 04), 61.17, 13.58);
			AddPoint(new DateTime(2014, 08, 05), 60.43, 13.61);
			AddPoint(new DateTime(2014, 08, 06), 59.82, 13.40);
			AddPoint(new DateTime(2014, 08, 07), 58.95, 13.16);
			AddPoint(new DateTime(2014, 08, 08), 59.27, 13.16);
			AddPoint(new DateTime(2014, 08, 11), 60.71, 13.36);
			AddPoint(new DateTime(2014, 08, 12), 59.85, 13.17);
			AddPoint(new DateTime(2014, 08, 13), 60.66, 13.80);
			AddPoint(new DateTime(2014, 08, 14), 61.07, 13.77);
			AddPoint(new DateTime(2014, 08, 15), 59.71, 13.65);
			AddPoint(new DateTime(2014, 08, 18), 60.99, 13.72);
			AddPoint(new DateTime(2014, 08, 19), 61.60, 13.72);
			AddPoint(new DateTime(2014, 08, 20), 61.33, 13.82);
			AddPoint(new DateTime(2014, 08, 21), 62.20, 13.86);
			AddPoint(new DateTime(2014, 08, 22), 61.65, 13.70);
			AddPoint(new DateTime(2014, 08, 25), 62.88, 13.88);
			AddPoint(new DateTime(2014, 08, 26), 63.49, 13.87);
			AddPoint(new DateTime(2014, 08, 27), 63.15, 13.89);
			AddPoint(new DateTime(2014, 08, 28), 62.16, 13.77);
			AddPoint(new DateTime(2014, 08, 29), 62.24, 13.83);
			AddPoint(new DateTime(2014, 09, 01), 61.88, 13.92);
			AddPoint(new DateTime(2014, 09, 02), 61.82, 13.92);
			AddPoint(new DateTime(2014, 09, 03), 62.90, 14.17);
			AddPoint(new DateTime(2014, 09, 04), 64.14, 14.34);
			AddPoint(new DateTime(2014, 09, 05), 65.17, 14.40);

			Points = new ReadOnlyObservableCollection<PriceCluster>(_Points);
		} // constructor

		// only to be called from the dispatcher thread!
		public void AddPoint(DateTime xDate, double xPriceVW, double xPriceDaimler) {
			_Points.Add(new PriceCluster(xDate, xPriceVW, xPriceDaimler));
		} //

	} // class
} // namespace

 

using System;

namespace Demo {
	public class PriceCluster {
		public DateTime Date { get; set; }
		public double PriceVW { get; set; }
		public double PriceDaimler { get; set; }

		public PriceCluster(DateTime xDate, double xPriceVW, double xPriceDaimler) {
			Date = xDate;
			PriceVW = xPriceVW;
			PriceDaimler = xPriceDaimler;
		} // constructor
	} // class
} // namespace

WPF Charts (Part 1)

Window

I was playing around with techniques and built a short chart demo. There are many tools out there to create charts. I prefer the good old WPF Toolkit solution on codeplex, which adds the namespace ‘System.Windows.Controls.DataVisualization.Chart’ and is supported by Microsoft. You can expect high compatibility at zero costs.
Do not confuse this one with the Extended WPF Toolkit, which is free software, but also offers a commercial solution.

Namespace

We are going to create various WPF charts in the coming weeks. The programming pattern series will continue at some point afterwards. What topics I choose is always closely related to my personal interests at that time. I find it hard to motivate myself otherwise.

This is a very simple example today. I added two NumericUpDown controls to add some flavor. Well, in the WPF Toolkit they are not called NumericUpDown anymore. There are corresponding DoubleUpDown/ DecimalUpDown/IntegerUpDown controls.

The lower DoubleUpDown control in this demo is linked to the upper one. And in turn the upper one is bound to a DataContext object property. This demonstrates a chain of bindings. Hence three objects are linked together holding the same value.

You can uncomment the prepared block in the XAML code. This will influence the line color and the line thickness. This template has its limits. It does not change the color of related objects. Anyway, it is a good start.

The chart has two curves. You can influence one by using any of the two DoubleUpDown controls.
The used ObservableCollection to store the curve points could be a post on its own. Basically, it is a WPF collection, which notifies WPF when you add or remove items from/to the list. But how do you update a chart, which only changes a value of a single point? The four methods to invalidate the drawing area are somewhat not showing the expected results.
You can set the DataContext to null and then set it back to your source. This is not the fastest way. But practically speaking, changing one value does not happen very often and setting the DataContext is quick and easy. Usually you only add or remove points. If you are looking for animations, they are dealt with differently in WPF. You should have a look into System.Windows.Media.Storyboard for that. In this example I chose to simply remove and add the affected point.
You don’t have to re-insert the point at the right collection position. I just did it to easily find the same point again. A simple Chart.Add() would work as well.

WPF will most likely not show the point removal on the screen. Tell me if I am wrong. I haven’t seen any impact. I guess the Dispatcher thread is blocked while you are using it on the WPF event call. A signal, which may happen right after the event finishes, will trigger the queued removal and addition in one go.

 

<Window x:Class="Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:tool="clr-namespace:Xceed.Wpf.Toolkit;assembly=Xceed.Wpf.Toolkit"
        xmlns:dv="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"        
        xmlns:local="clr-namespace:Demo"
        Title="MainWindow" Height="350" Width="525"
        Initialized="Window_Initialized">
    <Grid>
        <DockPanel LastChildFill="True">
            <tool:DoubleUpDown DockPanel.Dock="Top" Name="UpDown1" AllowSpin="True" 
                               Minimum="0" Maximum="100.5" Increment="0.5" ClipValueToMinMax="True"
                               DefaultValue="0" Watermark="enter a value"                                                        
                               MouseWheelActiveOnFocus="True" MouseWheelActiveTrigger="FocusedMouseOver"
                               FormatString="N3" ShowButtonSpinner="True" TextAlignment="Center"
                               Value="{Binding PriceOfDay3, Mode=OneWayToSource, FallbackValue=55.5 }" />

            <tool:DoubleUpDown DockPanel.Dock="Top" Name="UpDown2" AllowSpin="True" 
                               Minimum="0" Maximum="100.5" Increment="0.5" ClipValueToMinMax="True"
                               Value="{Binding Value, ElementName=UpDown1, Mode=TwoWay}"                                                        
                               MouseWheelActiveOnFocus="True" MouseWheelActiveTrigger="FocusedMouseOver"
                               FormatString="N3" ShowButtonSpinner="True" TextAlignment="Center"/>


            <dv:Chart Name="Chart1" Title="Test Chart" >

                <dv:LineSeries Title="Price" 
                            ItemsSource="{Binding Points, Delay=2500, IsAsync=False}"                               
                            IndependentValueBinding="{Binding Day}"
                            DependentValueBinding="{Binding Price}" >
                    <dv:LineSeries.DependentRangeAxis>
                        <dv:LinearAxis Orientation="Y" Title="Price"
                                    Minimum="50" Maximum="60" Interval="2" 
                                    ShowGridLines="True"/>
                    </dv:LineSeries.DependentRangeAxis>
                    <!--<dv:LineSeries.Template>
                        --><!-- change the line color to green and set the thickness --><!-- 
                        <ControlTemplate TargetType="dv:LineSeries">
                            <Canvas x:Name="PlotArea">
                                <Polyline x:Name="polyline"
                                          Points="{TemplateBinding Points}"                                           
                                          Style="{TemplateBinding PolylineStyle}"
                                          Stroke="Green" StrokeThickness="4" />
                            </Canvas>
                        </ControlTemplate>
                    </dv:LineSeries.Template>-->
                </dv:LineSeries>

                <dv:LineSeries Title="Tax"
                            ItemsSource="{Binding Points, Delay=2500, IsAsync=False}"
                            IndependentValueBinding="{Binding Day}"
                            DependentValueBinding="{Binding Tax}">
                    
                    <dv:LineSeries.DependentRangeAxis>
                        <dv:LinearAxis Orientation="Y" Title="Tax" Minimum="-10" Maximum="10" Interval="2.5"/>
                    </dv:LineSeries.DependentRangeAxis>
                </dv:LineSeries>
                <dv:Chart.Axes>                    
                    <dv:LinearAxis Orientation="X" Title="X-Axis" Interval="2" ShowGridLines="True"/>
                </dv:Chart.Axes>
            </dv:Chart>
        </DockPanel>
    </Grid>
</Window>
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls.DataVisualization.Charting;

namespace Demo {

  public partial class MainWindow : Window {
    public MainWindow() { InitializeComponent(); }

    public class DataPoint {
      public double Day { get; set; }
      public double Price { get; set; }
      public double Tax { get; set; }
    } // class

    public class ViewModel {
      private readonly Chart _Chart;
      public ObservableCollection<DataPoint> Points { get; private set; }

      public double PriceOfDay3 {
        get { lock (this) return Points[2].Price; }
        set {
          lock (this) {            
            DataPoint p = Points[2];
            p.Price = value;
            Points.Remove(p);
            Points.Insert(2, p);  // same position          
            //Points.Add(p); // append to the end
          }
        }
      } //

      public ViewModel(Chart xChart) {
        _Chart = xChart;
        Points = new ObservableCollection<DataPoint>();

        Points.Add(new DataPoint() { Day = 1.0, Price = 55, Tax = 2.0 });
        Points.Add(new DataPoint() { Day = 1.5, Price = 54, Tax = 1.0 });
        Points.Add(new DataPoint() { Day = 2.0, Price = 58, Tax = -1.0 });
        Points.Add(new DataPoint() { Day = 3.0, Price = 55.5, Tax = 0.0 });
        Points.Add(new DataPoint() { Day = 4.0, Price = 53, Tax = -2.0 });
      } // constructor

    } // class

    private void Window_Initialized(object sender, EventArgs e) {
      ViewModel lViewModel = new ViewModel(Chart1);
      DataContext = lViewModel;
    } //


  } // class
} // namespace

Singleton Pattern

Highlander

 

I did never really care about programming patterns. I had to come up with ideas when I needed them. And back in the Commodore 64 ages, when I wrote my first “Hello World” program in Basic, there was no book about patterns anyway.

When you only allow one object instance, that is called Singleton.

Have you ever come across Remoting? You send a request to a server. There is a choice between Singleton and SingleCall. It basically means that you:

  • Always use the same object or
  • create a new object for each request.

Let’s say you order 4 pints of beer using your tablet PC rather than calling the sexy, blond, and 18-year-old waitress. You obviously want 4 pints at once. Therefore the correct choice would be SingleCall; 4 distinct objects. The geeky waitress reacts. She responds via her high-tech cash register: “Who are you?”. She could ask that 10 times and unless you get upset, you would always give the same answer. Any good programmer realizes that this would only require one object instance – a Singleton.

There are several ways to do this. The conceptual idea is pretty easy. (Wikipedia has some more details.) In this post I am only giving you my favourite pattern, which should always work. You don’t need more unless you want to show off with some cheap stuff.

 

Example:

Remove the sealed keyword in case you need to make the class inheritable.
Notice the double-check if (_Instance == null) ...

The reason is:

  • The first check avoids entering the lock in 99.9999% of the calls. You save precious processor time.
  • The second check avoids an unlikely multithreading problem. A thread A could enter the lock and not finish before a thread B arrives. B has to wait in front of the lock. A exits the lock, now B enters the lock. Without the second check, B would now create a new instance. And this is, what we are trying to avoid.

 

using System;

namespace Singleton {

  public sealed class JustOne {
    private static readonly object _Lock = new object();
    private static JustOne _Instance = null;
    private JustOne() { }

    public static JustOne Instance {
      get {
        if (_Instance == null) {
          lock (_Lock) {
            if (_Instance == null) _Instance = new JustOne();
          }
        }
        return _Instance;
      }
    } //

    public new string GetHashCode() { return base.GetHashCode().ToString(); }
  } // class

  class Program {

    static void Main(string[] args) {
      JustOne A = JustOne.Instance;
      JustOne B = JustOne.Instance;
      Console.WriteLine("HashCode of object A: " + A.GetHashCode());
      Console.WriteLine("HashCode of object B: " + B.GetHashCode());      

      Console.ReadLine();
    } // main

  } // class
} // namespace

Facade Pattern

facade

Don’t shoot the messenger!

This is something that you are doing all the time. We just name it now. The Facade Pattern is a structural programming design pattern. To explain it in a nutshell:

A class contains many references to objects, which are invisible to the client. The client has a reduced command set to keep things simple.

The class receives the simple command and takes care about the real complexity behind the facade, which is presented to the client. Think of a cash dispenser. All you have to know is a few buttons. The legal structure, the buildings, the accounting, the risk etc. … all is invisible to the client, who only cares about getting cash at this very moment.

 

namespace FacadePattern {

  internal class Burger {
    public void Prepare() { }
    public void WarmUp() { }
    public void Wrap() { }
  } // class

  internal class Fries {
    public void Fry() { }
    public void KeepWarm() { }
  } // class

  internal class Drink {
    public enum eDrink { Coke, SevenUp, Orange, Apple, Milk, Tea, Coffee }
    public bool IsSugarFree { set; get; }
    public bool IsHot { set; get; }
    public void Fill(eDrink xDrink) { }
  } // class

  internal class Extras {
    public void AddSalt() { }
    public void AddKetchup() { }
    public void AddMayo() { }
    public void AddNapkin() { }
  } // class

  public class MealFacade {
    private Burger _Burger = new Burger();
    private Fries _Fries = new Fries();
    private Drink _Drink = new Drink();
    private Extras _Extras = new Extras();

    public void MakeClientHappy() {
      _Drink.IsHot = false;
      _Drink.IsSugarFree = true;
      _Drink.Fill(Drink.eDrink.Coke);
      _Fries.Fry();
      _Burger.Prepare();
      _Burger.Wrap();
      _Extras.AddKetchup();
      _Extras.AddSalt();
      _Extras.AddNapkin();
    } //
  } // class

  class Program {
    static void Main(string[] args) {
      MealFacade lBurgerMeal = new MealFacade();
      lBurgerMeal.MakeClientHappy();
    } // main

  } // class
} // namespace

Prototype Pattern

Clonomat

Sometimes the creation of new objects is time and resource intensive. You could create a thousand objects during the program initialization and park them on a queue. The object would then be ready when needed. And looking at the Garbage Collection process in more detail, you may notice that the Generation has probably changed by then. Your object requires less processor time the older it becomes.

But this is not the topic today. It just has a similar idea. We are talking about time and/or resource intensive object creation.

The Prototype Pattern is used for cloning objects. Cloning can be substantially faster than initializing objects from scratch. Let’s say object A did load a lot of data from a file. You don’t have to do the same for object B. Simply copy object A and amend some fields.

Here is the pattern:

public interface IChocolateBar {
  IChocolateBar Clone();
} // interface

public class MintChocolateBar : IChocolateBar {
  public readonly int ObjectNumber;
  public MintChocolateBar(int xObjectNumber) { ObjectNumber = xObjectNumber; }

  public IChocolateBar Clone() { return MemberwiseClone() as MintChocolateBar; } 
} // class

public class DarkChocolateBar : IChocolateBar {
  public readonly int ObjectNumber;
  public DarkChocolateBar(int xObjectNumber) { ObjectNumber = xObjectNumber; }

  public IChocolateBar Clone() { return MemberwiseClone() as DarkChocolateBar; } 
} // class

public class CloneFactory {
  public IChocolateBar get(IChocolateBar xChocolateBar) { return xChocolateBar.Clone(); } 
} // class

The pattern is not really satisfying. Is there something better? C# offers the ICloneable interface.

public class Shortcut : ICloneable  {
  public readonly int ObjectNumber;
  public Shortcut(int xObjectNumber) { ObjectNumber = xObjectNumber; }

  public object Clone() { return MemberwiseClone(); } 
} // class

The problem with this interface is the missing generic type. In fact the use is obsolete and Microsoft does not recommend it anymore. We therefore build our own implementation.

public class Shortcut2 {
  public readonly int ObjectNumber;
  public Shortcut2(int xObjectNumber) { ObjectNumber = xObjectNumber; }

  public Shortcut2 ShallowCopy() { return MemberwiseClone() as Shortcut2; } 
} // class

Using an interface like

interface IShallowCopy<T> {
  T IShallowCopy();
} // interface 

public class Shortcut3<T> : IShallowCopy<T> {
  public readonly int ObjectNumber;
  public Shortcut3(int xObjectNumber) { ObjectNumber = xObjectNumber; }

  public T ShallowCopy() { return MemberwiseClone() as T; } 
} // class

is nonsense. You cannot compile and instantiate this.
What else could we do? A static clone method quickly leads us back to a factory similar type. Hence we end up with something that we were having at the very beginning today. Just keep your solutions generic and you should be fine with this pattern.

MemberwiseClone()

We were using MemberwiseClone() to keep the example source code simple. The following is important to know:

MemberwiseClone() is a shallow copy. If a field of the copied object is a reference type, the reference is copied but the referred object is not; therefore, the original object and its clone refer to the same object.

To avoid the same references in the clone, you have to write your own Deep Copy or Lazy Copy. Your clone method must clone the object and objects inside the object.

A practical example:

Stock exchange orders need to be sent with a minimal delay. The fight for nanoseconds is tremendous. Companies even shorten the physical server cables to the exchanges in order to increase speed. One foot in length equals roughly one nanosecond these days.
The object creation for orders takes too long in the proprietary business. It is meaningful to generate orders before any decision is made. The next order sits in a queue of clones. When the machine decides to trade, then the order is taken from that queue and a few fields are populated/overridden. These are eg. trade size and price. There is no memory allocation at this late stage. As said, each nanosecond counts.

Full source code

Notice that

Console.WriteLine("ObjectNumber = " + lMintClone.ObjectNumber); 

prints the same number as the original object. There are two objects at two locations in the RAM. Therefore the HashCode is different. Still the field content is the same.

using System;

namespace PrototypePattern {

  public interface IChocolateBar {
    IChocolateBar Clone();
  } // interface

  public class MintChocolateBar : IChocolateBar {
    public readonly int ObjectNumber;
    public MintChocolateBar(int xObjectNumber) { ObjectNumber = xObjectNumber; }

    public IChocolateBar Clone() { return MemberwiseClone() as MintChocolateBar; }

  } // class

  public class DarkChocolateBar : IChocolateBar {
    public readonly int ObjectNumber;
    public DarkChocolateBar(int xObjectNumber) { ObjectNumber = xObjectNumber; }

    public IChocolateBar Clone() { return MemberwiseClone() as DarkChocolateBar; }

  } // class

  public class CloneFactory {
    public IChocolateBar get(IChocolateBar xChocolateBar) { return xChocolateBar.Clone(); }
  } // class


  // IClonable is non-generic
  public class Shortcut : ICloneable {
    public readonly int ObjectNumber;
    public Shortcut(int xObjectNumber) { ObjectNumber = xObjectNumber; }

    public object Clone() { return MemberwiseClone(); } 
  } // class

  public class Shortcut2 {
    public readonly int ObjectNumber;
    public Shortcut2(int xObjectNumber) { ObjectNumber = xObjectNumber; }

    public Shortcut2 ShallowCopy() { return MemberwiseClone() as Shortcut2; } 
  } // class

  class Program {

    static void Main(string[] args) {
      CloneFactory lCloneFactory = new CloneFactory();
      MintChocolateBar lMint = new MintChocolateBar(1);
      MintChocolateBar lMintClone = lCloneFactory.get(lMint) as MintChocolateBar;
      Console.WriteLine("Original object: ");
      Console.WriteLine("HashCode = " + lMint.GetHashCode());
      Console.WriteLine("ObjectNumber = " + lMint.ObjectNumber);
      Console.WriteLine();
      Console.WriteLine("Clone: ");
      Console.WriteLine("HashCode = " + lMintClone.GetHashCode());
      Console.WriteLine("ObjectNumber = " + lMintClone.ObjectNumber);  // !!!
      Console.WriteLine();
      Console.WriteLine("Are the objects the same? " + (lMint == lMintClone));

      DarkChocolateBar lDark = new DarkChocolateBar(2);
      DarkChocolateBar lDarkClone = lCloneFactory.get(lDark) as DarkChocolateBar;
      Console.WriteLine();
      Console.WriteLine();
      Console.WriteLine();
      Console.WriteLine("Dark chocolate: ");
      Console.WriteLine("Are the objects the same? " + (lMint == lMintClone));


      // old school
      Shortcut lShort = new Shortcut(3);
      Shortcut lShortClone = lShort.Clone() as Shortcut;
      Console.WriteLine();
      Console.WriteLine("ICloneable: ");
      Console.WriteLine("Are the objects the same? " + (lShort == lShortClone));

      Console.ReadLine();
    } //

  } // class

} // namespace

example output:
Original object:
HashCode = 62125865
ObjectNumber = 1

Clone:
HashCode = 44200505
ObjectNumber = 1

Are the objects the same? False

Dark chocolate:
Are the objects the same? False

ICloneable:
Are the objects the same? False

Wikipedia

Builder Pattern

Car

Who does not know the StringBuilder in namespace System.Text?
The purpose of the StringBuilder is to construct a string much faster than using the operator overload “+”. Strings are immutable. Each time you use the overloaded operator, a new object is created. That slows down the creation of the final object. Here is a quick benchmark:

using System;
using System.Diagnostics;
using System.Text;

namespace demo {
  class Program {

    static void Main(string[] args) {
      Stopwatch lStopwatch = new Stopwatch();
      lStopwatch.Start();
      string s = "";
      for (int i = 0; i < 100000; i++) s += "x";
      lStopwatch.Stop();
      Console.WriteLine("Operator, elapsed ms: " + lStopwatch.ElapsedMilliseconds);

      StringBuilder lStringBuilder = new StringBuilder();
      lStopwatch.Restart();
      for (int i = 0; i < 100000; i++) lStringBuilder.Append("x");
      lStopwatch.Stop();
      Console.WriteLine("StringBuilder, elapsed ms: " + lStopwatch.ElapsedMilliseconds);

      Console.ReadLine();
    } //

  } // class
} // namespace

example output:
Operator, elapsed ms: 3029
StringBuilder, elapsed ms: 1

The StringBuilder supports method chaining. You can shorten your source code and write:

string s = new StringBuilder()
  .Append("a")
  .Append("b")
  .Append("c")
  .Append("d")
  .ToString();

instead of

StringBuilder lStringBuilder = new StringBuilder();
lStringBuilder.Append("a");
lStringBuilder.Append("b");
lStringBuilder.Append("c");
lStringBuilder.Append("d");
string s = lStringBuilder.ToString();

In general the purpose of a Builder is to separate complex object construction from its representation.

We leave the idea of strings behind and build something entirely different. This time it is not about speed, it is more about avoiding thousands of parameters in constructors and making complexity look a little bit easier. Two weeks ago I was using BMW AG to give an example for an Abstract Factory. Let’s stay loyal to cars and build classes for the production assembly.

using System;

namespace demo {

  [Flags]
  public enum eRadio { GPS = 1, MP3 = 2, Screen = 4, Phone = 8 }
  public enum eColor { NotSet = 0, Silver, Red, Blue, White, Black }

  class Program {

    public class Car {
      public readonly eRadio Radio;
      public readonly eColor Color;
      public readonly double Displacement;
      public readonly int Doors;
      public readonly bool Hatchback;
      public readonly bool LeatherSeats;

      internal Car(eRadio xRadio, eColor xColor, double xDisplacement, int xDoors, bool xHatchback, bool xLeatherSeats) {
        Radio = xRadio;
        Color = xColor;
        Displacement = xDisplacement;
        Doors = xDoors;
        Hatchback = xHatchback;
        LeatherSeats = xLeatherSeats;
      } // constructor

    } // class

    public class CarBuilder {

      private eRadio _Radio = 0;
      private eColor _Color = eColor.NotSet;
      private double _Displacement = 0.0;
      private uint _Doors = 1;
      private bool? _Hatchback = null;
      private bool? _LeatherSeats = null;

      internal CarBuilder SetColor(eColor xValue) { _Color = xValue; return this; }
      internal CarBuilder SetDisplacement(double xValue) { _Displacement = xValue; return this; }
      internal CarBuilder SetDoors(uint xValue) { _Doors = xValue; return this; }
      internal CarBuilder SetHatchback(bool xValue) { _Hatchback = xValue; return this; }
      internal CarBuilder SetRadio(eRadio xValue) { _Radio = xValue; return this; }
      internal CarBuilder SetLeatherSeats(bool xValue) { _LeatherSeats = xValue; return this; }
      internal Car Create() {
        if (_Hatchback == null) throw new Exception("Hatchback not set");
        if (_LeatherSeats == null) throw new Exception("LeatherSeats not set");
        if (_Doors < 1 || _Doors > 6) throw new Exception("number of Doors invalid");
        // ... etc.

        return new Car(_Radio, _Color, _Displacement, (int)_Doors, (bool)_Hatchback, (bool)_LeatherSeats);
      }
    } // class

    static void Main(string[] args) {

      Car lCar = new CarBuilder()
        .SetColor(eColor.Silver)
        .SetDisplacement(2.2)
        .SetDoors(5)
        .SetHatchback(true)
        .SetRadio(eRadio.GPS | eRadio.MP3 | eRadio.Screen)
        .SetLeatherSeats(true)
        .Create();

      Console.ReadLine();
    } // main

  } // class
} // namespace

http://en.wikipedia.org/wiki/Builder_pattern

Adapter Pattern

Adapter
Once again it sounds more difficult than it actually is.

What is an Adapter?

An Adapter sits in between two objects that cannot be changed and cannot talk to each other. It is like the American and British power socket:

  • The socket shape and voltage are different.
  • You cannot change the situation. The source and the target are static.
  • An adapter can connect the two systems.

Adapters can also connect to several source or target objects. Let’s imagine a database is split in two. An Adapter could connect to the two separate databases and return the result as if it was just one. Using several source objects is uncommon. It does make sense, when events from two or more sources need to occur before the access to the target object can take place.

MultiAdapter

I’d say that the Adapter pattern is not more than a workaround, it is a compromise to avoid other problems. There is a difference between chewing gum and glue. Both can hold pieces together … somehow.
Imagine a British datacenter with 1000 (US->UK) power socket adapters. It would work, but don’t wonder, when someone stumbles over a adapter and disconnects something important.

(The WPF value converter is similar, not the same though. The converter eg. accepts a string and returns a double.)

Here is a short example:

namespace demo {

  public interface IPrinterDriver {
    void Print(string xText);
  } // interface

  // Client
  public class PrinterDriver : IPrinterDriver {
    private readonly IPrinterDriver _Driver;
    public PrinterDriver(IPrinterDriver xDriver) { _Driver = xDriver; }

    public void Print(string xText) { _Driver.Print(xText); }
  } // class

  // Adaptee
  public class Windows3_1_MatrixPrinterDriver {
    public void Init(string xCommandSequence) { ... }
    public void ParkPrintHead() { ... }
    public void Print(string xText) { ... }
  } // class

  // Adapter
  public class Win8_to_Win31_PrinterDriverAdapter : IPrinterDriver {
    private readonly Windows3_1_MatrixPrinterDriver _Driver = new Windows3_1_MatrixPrinterDriver();
    public void Print(string xText) {
      _Driver.Init("§50asabpwebts.dae§");
      _Driver.Print(xText);
      _Driver.ParkPrintHead();
    } //
  } // class

  class Program {

    static void Main(string[] args) {
      Win8_to_Win31_PrinterDriverAdapter lAdapter = new Win8_to_Win31_PrinterDriverAdapter();
      PrinterDriver lDriver = new PrinterDriver(lAdapter);
      lDriver.Print("Hello World!");
    } //

  } // class
} // namespace

In many examples the adapter inherits the Adaptee AND the Interface, which would then look like this:

public class Win8_to_Win31_PrinterDriverAdapter : IPrinterDriver, Windows3_1_MatrixPrinterDriver { ... }

I disagree with such approach and rather use create a private readonly field instead. It is easier to avoid naming conflicts then.

Abstract Factory Pattern

BMW

 

There are many definitions on the internet of what the factory pattern would/should be. It is important to comply with your professor while you study at a university. Besides that programming is an art. It has nothing to do with naming conventions. Mona Lisa could be named Paul, you see. Only posh people care about technique naming. But if you paint a picture yourself, you don’t care about any technique naming convention at all; you only care about your results. And if people like it, then you are successful. It is the same with programming.

The Factory Method Pattern usually refers to a class that creates just one type of object.
The Abstract Factory Pattern usually refers to a class that can create various types of objects, which have something in common.

Besides that we have a lot of gibberish about some 10 and more years old pattern.

I could add a lot of code here, but it should suffice to just get the idea of it. Have a look at the above picture. An abstract class/interface is used as the base class for country specific factories. These factories create the objects.
The Z4 Model is built in Germany and China (just for demonstration purposes). Basically they are the same besides the difference between the Chinese and German regulations.

Now, translate that to your own business case. You may need a Factory that creates code for your Unix, Mac, and Windows system. On top of that you may have to create two distinct objects for your test and prod environment.

Other explanations are here or here.

Follow

Get every new post delivered to your Inbox.