Blog Archives
JSON to LINQ to JSON
It is about 9:30pm and I am sitting on the train home. It has been quite tough organizing my life in the last months. This is not the promised follow-up on OpenGL. There has been no time to read any book.
Anyway, this post shows how to run LINQ queries on JSON. The use can be quite broad. Imagine a shell command executing your LINQ on any JSON file. You can query into depth and build complex return tree structures with 2 or more levels. LINQ itself is highly flexible. And – just to give it the right flair – it returns results in the good old JSON format.
You could even go a bit further and insert more than just a LINQ query. In theory, you could add any C# code at run-time. The changes to this program would be minuscule.
I have sufficiently commented the code. There is a minimum overhead to get the job done.
How it works:
- The JSON data is imported using the JavaScriptSerializer, which is part of the System.Web.Extensions library reference. We try to force the result into a Dictionary<string, object>. Do not lazily use ‘object’. Some type information would get lost. I played with this and got eg. arrays instead of ArrayLists.
- The resulting structure is then used to build classes. These are converted to legible C# source code.
- The LINQ command is inserted. You can see the result in the TextBox titled ‘C# source code output’.
- A compiler instance is created and some library references are added. We compile the generated source code at run-time and then execute the binary.
- A standard JSON object is returned, rudimentary formatted and displayed as the final result.
Btw. I have inserted a horizontal and a vertical GridSplitter into the WPF XAML. You can easily change the size of the TextBoxes while playing with this tool. A few JSON examples were also added.
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="LinqJson.MainWindow" Title="JSON LINQ JSON" Height="Auto" Width="876" d:DesignHeight="695"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="1*"/> <RowDefinition Height="43*"/> <RowDefinition Height="30"/> <RowDefinition Height="150*"/> <RowDefinition Height="30"/> <RowDefinition Height="150*"/> <RowDefinition Height="10*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="10*"/> <ColumnDefinition Width="250*"/> <ColumnDefinition Width="10"/> <ColumnDefinition Width="250*"/> <ColumnDefinition Width="10*"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Button Content="convert" Width="50" Height="30" Click="Button_Click" Grid.Row="1" HorizontalAlignment="Left" Grid.Column="1" Margin="0,8,0,7"/> <StackPanel Grid.Column="1" Grid.Row="2" VerticalAlignment="Bottom" HorizontalAlignment="Left" Orientation="Horizontal" Height="27" > <Label Content="JSON input" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="0,1" /> <ComboBox Width="240" Margin="50,3,3,3" SelectionChanged="ComboBox_SelectionChanged" VerticalAlignment="Bottom"> <ComboBoxItem Name="ex1">Example 1</ComboBoxItem> <ComboBoxItem Name="ex2">Example 2</ComboBoxItem> <ComboBoxItem Name="ex3">Example 3</ComboBoxItem> <ComboBoxItem Name="ex4">Example 4</ComboBoxItem> </ComboBox> </StackPanel> <TextBox x:Name="JsonIn" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Column="1" Grid.Row="3"/> <GridSplitter Grid.Column="2" Grid.RowSpan="999" HorizontalAlignment="Stretch" Width="10" Background="Transparent" ResizeBehavior="PreviousAndNext"/> <GridSplitter Grid.Row="4" Grid.ColumnSpan="999" VerticalAlignment="Top" HorizontalAlignment="Stretch" Height="10" Background="Transparent" ResizeBehavior="PreviousAndNext" /> <Label Content="JSON output" Grid.Column="3" Grid.Row="2" VerticalAlignment="Bottom" /> <TextBox x:Name="JsonOut" VerticalAlignment="Stretch" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Column="3" Grid.Row="3" /> <Label Content="C# source code output" Grid.Column="3" Grid.Row="4" VerticalAlignment="Bottom" /> <TextBox x:Name="CSharpSourceCode" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Column="3" Grid.Row="5" /> <Label Content="LINQ input" Grid.Column="1" Grid.Row="4" VerticalAlignment="Bottom"/> <TextBox x:Name="LinqIn" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Column="1" Grid.Row="5" /> </Grid> </Window>
using Microsoft.CSharp; using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Windows; using System.Windows.Controls; namespace LinqJson { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } // constructor private void Button_Click(object sender, RoutedEventArgs e) { // -------------------------------------------------- // get the JSON input // -------------------------------------------------- string lJsonIn = JsonIn.Text; // -------------------------------------------------- // construct the C# source code (class hierarchy) // -------------------------------------------------- Converter lConverter = new Converter(); Dictionary<string, object> lTree = lConverter.JsonToDictionary(lJsonIn); lConverter.DictionariesToClasses("root", 0, lTree); var lStringBuilder = new StringBuilder(); lConverter.BuildClasses(lStringBuilder); string lCSharpSourceCode = lConverter.GetUsings() + lStringBuilder.ToString(); // -------------------------------------------------- // add the LINQ command to the source code // -------------------------------------------------- string lLinq = LinqIn.Text; string lEntryPoint = "\n\n"; lEntryPoint += "public class baseClass {\n"; lEntryPoint += " public static object executeLinq(string xJson) {\n"; lEntryPoint += " xJson = xJson.Trim();"; lEntryPoint += " if (xJson[0] == '[') xJson = \"{home: \" + xJson + \"}\";"; lEntryPoint += " var lSerializer = new JavaScriptSerializer();\n"; lEntryPoint += " var root = lSerializer.Deserialize<Class_root>(xJson);\n"; lEntryPoint += " var lResult = " + lLinq.Replace("\n", "\n ") + ";\n"; lEntryPoint += " return lSerializer.Serialize(lResult);\n"; lEntryPoint += " }\n"; lEntryPoint += "}\n"; lCSharpSourceCode += lEntryPoint; // -------------------------------------------------- // display the source code // -------------------------------------------------- CSharpSourceCode.Text = lCSharpSourceCode; // -------------------------------------------------- // compile the source code // -------------------------------------------------- var lProviderOptions = new Dictionary<string, string>(); lProviderOptions.Add("CompilerVersion", "v4.0"); var lCSharpCodeProvider = new CSharpCodeProvider(lProviderOptions); var lCompilerParameters = new CompilerParameters(); lCompilerParameters.ReferencedAssemblies.Add("System.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Core.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Data.Linq.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Threading.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Web.Extensions.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Xml.Linq.dll"); lCompilerParameters.GenerateInMemory = true; lCompilerParameters.GenerateExecutable = false; // not required, we don't have a Main() method lCompilerParameters.IncludeDebugInformation = true; var lCompilerResults = lCSharpCodeProvider.CompileAssemblyFromSource(lCompilerParameters, lCSharpSourceCode); if (lCompilerResults.Errors.HasErrors) { var lError = new StringBuilder(); foreach (CompilerError lCompilerError in lCompilerResults.Errors) { lError.AppendLine(lCompilerError.ErrorNumber + " => " + lCompilerError.ErrorText + Environment.NewLine); } JsonOut.TextWrapping = TextWrapping.Wrap; JsonOut.Text = lError.ToString(); return; } JsonOut.TextWrapping = TextWrapping.NoWrap; // -------------------------------------------------- // execute the compiled code // -------------------------------------------------- Assembly lAssembly = lCompilerResults.CompiledAssembly; Type lProgram = lAssembly.GetType("baseClass"); MethodInfo lMethod = lProgram.GetMethod("executeLinq"); object lQueryResult = lMethod.Invoke(null, new object[] { lJsonIn }); // returns a JSON string object // -------------------------------------------------- // rudimentary JSON output formatting // -------------------------------------------------- string lJsonOut = lQueryResult.ToString(); lJsonOut = lJsonOut.Replace(",", ",\n"); lJsonOut = lJsonOut.Replace(",{", ",{\n"); lJsonOut = lJsonOut.Replace("]", "]\n"); JsonOut.Text = lJsonOut; } // private void ComboBox_SelectionChanged(object xSender, SelectionChangedEventArgs e) { var lComboBox = xSender as ComboBox; switch (lComboBox.SelectedIndex) { case 0: JsonIn.Text = "{\n \"number\": 108.541,\n \"datetime\": \"1975-03-13T10:30:00\" ,\n \"serialnumber\": \"SN1234\",\n \"more\": {\n \"field1\": 123,\n \"field2\": \"hello\"\n },\n \"array\": [\n {\"x\": 2.0},\n {\"x\": 3.0},\n {\"x\": 4.0}\n ]\n}"; LinqIn.Text = "from a in root.array\nwhere a.x > 2.0M\nselect a"; break; case 1: JsonIn.Text = "[ 1, 9, 5, 7, 1, 4 ]"; LinqIn.Text = "from a in root.home\nwhere ((a == 4) || (a == 1))\nselect a"; break; case 2: JsonIn.Text = "{myLuckyNumbers: [ 1, 9, 5, 26, 7, 1, 4 ]}"; LinqIn.Text = "from x in root.myLuckyNumbers\nwhere x % 2 == 0\nselect new { simple=x, square=x*x, text=\"Field\"+x.ToString(), classInClass=new {veryOdd=x/7.0, lol=\":D\"}}"; break; case 3: string s; s = "{\"mainMenu\": {\n"; s += " \"info\": \"Great tool.\",\n"; s += " \"value\": 100.00,\n"; s += " \"menu\": {\n"; s += " \"subMenu\": [\n"; s += " {\"text\": \"New\", \"onclick\": \"NewObject()\"},\n"; s += " {\"text\": \"Open\", \"onclick\": \"Load()\"},\n"; s += " {\"text\": \"Close\", \"onclick\": \"ByeBye()\"},\n"; s += " {\"text\": \"NNN\", \"onclick\": \"Useless()\"}\n"; s += " ]}\n"; s += "}}\n"; JsonIn.Text = s; LinqIn.Text = "root.mainMenu.menu.subMenu.Where(t => t.text.StartsWith(\"N\")).Last();"; break; default: break; } } // } // class } // namespace
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Script.Serialization; // requires reference to System.Web.Extensions, used by the JavaScriptSerializer namespace LinqJson { public class Converter { private Dictionary<string, object> _Classes = new Dictionary<string, object>(); public string GetUsings() { string s; s = "using System;\n"; s += "using System.Collections.Generic;\n"; s += "using System.Linq;\nusing System.Text;\n"; s += "using System.Threading.Tasks;\n"; s += "using System.Collections;\n"; s += "using System.Web.Script.Serialization;\n\n"; return s; } // public Dictionary<string, object> JsonToDictionary(string xJson) { xJson = xJson.Trim(); if (xJson[0] == '[') xJson = "{home: " + xJson + "}"; // nameless arrays cannot be converted to dictionaries var lJavaScriptSerializer = new JavaScriptSerializer(); try { return lJavaScriptSerializer.Deserialize<Dictionary<string, object>>(xJson); } catch (Exception) { return null; } } // public void BuildClasses(StringBuilder xStringBuilder) { foreach (var lClass in _Classes) { Dictionary<string, object> lMembers = lClass.Value as Dictionary<string, object>; if (lMembers == null) continue; if (lMembers.Count <= 0) continue; xStringBuilder.Append("public class Class_"); xStringBuilder.Append(lClass.Key); xStringBuilder.AppendLine(" {"); foreach (var lMember in lMembers) { object lValue = lMember.Value; string lKey = lMember.Key; Type lType = (lValue == null) ? typeof(object) : lMember.Value.GetType(); xStringBuilder.Append(new String(' ', 2)); xStringBuilder.Append("public "); if (lType.IsValueType || (lValue is string)) { xStringBuilder.Append(lType.Name); xStringBuilder.Append(" "); xStringBuilder.Append(lKey); xStringBuilder.AppendLine(";"); } else if (lValue is Dictionary<string, object>) { xStringBuilder.Append("Class_"); xStringBuilder.Append(lKey); xStringBuilder.Append(" "); xStringBuilder.Append(lKey); xStringBuilder.AppendLine(";"); } else if (lValue is ArrayList) { ArrayList lArrayList = lValue as ArrayList; var lMemberType = ArrayListType(lArrayList); // differentiate between the contents of the list if (lMemberType.IsValueType || (lMemberType.Name == "String")) { //xStringBuilder.Append(lMemberType.Name.Replace("`2")); // Dictionaries use name "Dictionary`2" xStringBuilder.Append(" List<"); xStringBuilder.Append(lMemberType.Name); xStringBuilder.Append("> "); xStringBuilder.Append(lKey); xStringBuilder.AppendLine(";"); } else { // a class xStringBuilder.Append("List<Class_" + lKey + "> "); xStringBuilder.Append(lKey); xStringBuilder.AppendLine(";"); } } } xStringBuilder.AppendLine(" }"); xStringBuilder.AppendLine(); } } // public void DictionariesToClasses(string xRootName, int xIndent, object xObject) { if (xObject == null) return; Type lType = xObject.GetType(); if (lType.IsValueType || (xObject is string)) return; var lDictionary = xObject as Dictionary<string, object>; if (lDictionary != null) { object lObj; if (!_Classes.TryGetValue(xRootName, out lObj)) { _Classes.Add(xRootName, lDictionary); } else { foreach (var lKeyValuePair in lDictionary) { // This is a weakness of the program. // Two JSON objects must not use the same name. // We would have to compare the JSON objects and determine wether to create multiple or just one C# class. _Classes[lKeyValuePair.Key] = lKeyValuePair.Value; // object type will be overridden !!!!!! } return; } foreach (var lKeyValuePair in lDictionary) { DictionariesToClasses(lKeyValuePair.Key, xIndent, lKeyValuePair.Value); } return; } var lArrayList = xObject as ArrayList; if (lArrayList != null) { object lObj; if (!_Classes.TryGetValue(xRootName, out lObj)) { lDictionary = new Dictionary<string, object>(); _Classes.Add(xRootName, lDictionary); } else lDictionary = lObj as Dictionary<string, object>; var lElementType = ArrayListType(lArrayList); if (lElementType == typeof(Dictionary<string, object>)) { var lList = lArrayList.Cast<Dictionary<string, object>>().ToList(); // upgrade our object to have stronger types foreach (var lDict in lList) { foreach (var lKeyValuePair in lDict) { lDictionary[lKeyValuePair.Key] = lKeyValuePair.Value; // object type will be overridden !!!!!! } } foreach (var lKeyValuePair in lDictionary) { DictionariesToClasses(lKeyValuePair.Key, xIndent, lKeyValuePair.Value); } } return; } } // private void Append(StringBuilder xStringbuilder, int xIndent, string xString) { xStringbuilder.Append(new String(' ', xIndent)); xStringbuilder.Append(xString); } // private void Newline(StringBuilder xStringbuilder) { xStringbuilder.AppendLine(); } // private Type ArrayListType(ArrayList xArrayList) { var lTypes = new Dictionary<string, Type>(); Type lType; string lTypeName; foreach (object o in xArrayList) { if (o == null) lType = typeof(object); else lType = o.GetType(); lTypeName = lType.Name; if (!lTypes.ContainsKey(lTypeName)) lTypes.Add(lTypeName, lType); } if (lTypes.Count == 1) return lTypes.Values.First(); // distinct return typeof(object); } // } // class } // namespace
Clipboard to Text To Speech (TTS)
Since two years I am using Text To Speech (TTS). The quality has improved a lot. The spoken text can be understood and the pronunciations have reached good levels; not perfect though. Since Windows 7 there is no need to pay for a professional voice anymore. The Microsoft Windows system voices are sufficient.
First, I wanted to build my own add-on for the Firefox browser. But I quickly realised that there are too many constraints. I am using a nice tool on my Samsung Note 4 to listen to web site texts on a daily basis. That works out very well.
Nevertheless, this tool here is for home PCs.
ClipboardTTS monitors the windows clipboard and displays the current text in a WPF TextBox. In case the CheckBox “Auto” is checked the application starts speaking the text immediately. You can also generate WAV files. Adding this feature only took a few extra lines, otherwise it would not have been worth it. There are only a few use cases.
The Clipboard class offered in .Net does not provide events to monitor clipboard changes. We therefore have to use the old fashioned 32bit Windows functions. Therefore the codes section starts with imports from the windows “user32.dll”. With these you can subscribe to updates. The event WinProc notifies the application about changes. Most of these messages are disregarded in this application. We are only interested in two types of them. The first one is the Windows clipboard chain WM_CHANGECBCHAIN. You have to store the following window of the system clipboard chain, because we must forward messages to that one. This is a weird technology, but who knows what it is good for. For sure it simplifies suppressing messages without the need for any cancellation flag.
WM_DRAWCLIPBOARD is the other type we are interested in. This message tells you that the clipboard context has changed. Have a look at the C# code, you’ll quickly understand.
You could argue that the .Net Clipboard class is not needed, after all the user32.dll can do everything we need. Well, I think we should include as much .Net as possible. This is the right way to stick to the future.
Don’t forget to reference the System.Speech library in Visual Studio.
The application itself is pretty short. This is due to two facts. Windows is offering good SAPI voices and acceptable .Net support to use these voices.
http://msdn.microsoft.com/en-us/library/ee125077%28v=vs.85%29.aspx
I don’t see the point to implement an add-on for Firefox to follow the multi-platform approach. Do I have to re-invent the wheel? And check out the existing add-ons. You can hardly understand the spoken text. Some of these add-ons offer multi-language support. Yeah, but come on! You cannot understand a word. We are not in the 1990s anymore. Computers have learnt speaking very well. What is the language support good for if you cannot understand anything?
<Window x:Class="ClipboardTTS.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ClipboardTTS" Height="140" Width="263" Topmost="True" Loaded="Window_Loaded" Closed="Window_Closed"> <DockPanel LastChildFill="True"> <DockPanel DockPanel.Dock="Top" LastChildFill="False"> <CheckBox Name="Checkbox_OnOff" DockPanel.Dock="Left" Content="Auto" Margin="5" ToolTip="Speak as soon as the clipboard text changes"/> <Button Content="Say it" DockPanel.Dock="Right" Click="Button_SayIt_Click" Width="50" Margin="5" ToolTip="Start/Stop speaking"/> <Button Name="Button_Save" Content="Save" DockPanel.Dock="Right" Click="Button_Save_Click" Width="50" Margin="5" ToolTip="Create WAV sound file"/> </DockPanel> <ComboBox Name="ComboBox_Voices" DockPanel.Dock="Top" SelectionChanged="ComboBox_Voices_SelectionChanged" ToolTip="Voice"/> <Slider Name="Slider_Volumne" DockPanel.Dock="Top" Minimum="0" Maximum="100" Value="50" ValueChanged="Slider_Volumne_ValueChanged" ToolTip="Volume" /> <TextBox Name="TextBox_Clipboard" TextChanged="TextBox_Clipboard_TextChanged" > hello world ! </TextBox> </DockPanel> </Window>
using System; using System.Collections.ObjectModel; using System.Runtime.InteropServices; using System.Speech.Synthesis; using System.Windows; using System.Windows.Interop; using System.Windows.Media; namespace ClipboardTTS { public partial class MainWindow : Window { private const int WM_DRAWCLIPBOARD = 0x0308; // change notifications private const int WM_CHANGECBCHAIN = 0x030D; // another window is removed from the clipboard viewer chain private const int WM_CLIPBOARDUPDATE = 0x031D; // clipboard changed contents [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SetClipboardViewer(IntPtr xHWndNewViewer); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern bool ChangeClipboardChain(IntPtr xHWndRemove, IntPtr xHWndNewNext); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SendMessage(IntPtr xHWnd, int xMessage, IntPtr xWParam, IntPtr xLParam); private IntPtr _HWndNextViewer; // next window private HwndSource _HWndSource; // this window private string _Text = string.Empty; private SpeechSynthesizer _SpeechSynthesizer = new SpeechSynthesizer(); public MainWindow() { InitializeComponent(); } // constructor private void StartListeningToClipboard() { WindowInteropHelper lWindowInteropHelper = new WindowInteropHelper(this); _HWndSource = HwndSource.FromHwnd(lWindowInteropHelper.Handle); _HWndSource.AddHook(WinProc); _HWndNextViewer = SetClipboardViewer(_HWndSource.Handle); // set this window as a viewer } // private void StopListeningToClipboard() { ChangeClipboardChain(_HWndSource.Handle, _HWndNextViewer); // remove from cliboard viewer chain _HWndNextViewer = IntPtr.Zero; _HWndSource.RemoveHook(WinProc); } // private void SayIt(string xText) { if (string.IsNullOrWhiteSpace(xText)) return; _SpeechSynthesizer.Volume = (int)Slider_Volumne.Value; _SpeechSynthesizer.SpeakAsync(xText); } // private IntPtr WinProc(IntPtr xHwnd, int xMessageType, IntPtr xWParam, IntPtr xLParam, ref bool xHandled) { switch (xMessageType) { case WM_CHANGECBCHAIN: if (xWParam == _HWndNextViewer) _HWndNextViewer = xLParam; else if (_HWndNextViewer != IntPtr.Zero) SendMessage(_HWndNextViewer, xMessageType, xWParam, xLParam); break; case WM_DRAWCLIPBOARD: SendMessage(_HWndNextViewer, xMessageType, xWParam, xLParam); processWinProcMessage(); break; } return IntPtr.Zero; } // private void processWinProcMessage() { if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(processWinProcMessage); return; } if (!Clipboard.ContainsText()) return; string lPreviousText = _Text; _Text = Clipboard.GetText(); if (_Text.Equals(lPreviousText)) return; // do not play the same text again InsertTextIntoTextBox(_Text); if (Checkbox_OnOff.IsChecked.Value) SayIt(_Text); } // private void InsertTextIntoTextBox(string xText) { if (!TextBox_Clipboard.Dispatcher.CheckAccess()) { TextBox_Clipboard.Dispatcher.Invoke(() => InsertTextIntoTextBox(xText)); return; } TextBox_Clipboard.Text = xText; } // private void Button_SayIt_Click(object xSender, RoutedEventArgs e) { if (_SpeechSynthesizer.State == SynthesizerState.Speaking) { _SpeechSynthesizer.SpeakAsyncCancelAll(); return; } SayIt(TextBox_Clipboard.Text); } // private void TextBox_Clipboard_TextChanged(object xSender, System.Windows.Controls.TextChangedEventArgs e) { _Text = TextBox_Clipboard.Text; } // private void Window_Loaded(object xSender, RoutedEventArgs e) { ReadOnlyCollection<InstalledVoice> lVoices = _SpeechSynthesizer.GetInstalledVoices(); if (lVoices.Count < 1) return; foreach (InstalledVoice xVoice in lVoices) { ComboBox_Voices.Items.Add(xVoice.VoiceInfo.Name); } ComboBox_Voices.SelectedIndex = 0; StartListeningToClipboard(); } // private void Window_Closed(object xSender, EventArgs e) { StopListeningToClipboard(); } // private void ComboBox_Voices_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { string xVoice = ComboBox_Voices.SelectedItem as string; if (string.IsNullOrWhiteSpace(xVoice)) return; _SpeechSynthesizer.SelectVoice(xVoice); } // private void Slider_Volumne_ValueChanged(object xSender, RoutedPropertyChangedEventArgs<double> e) { _SpeechSynthesizer.Volume = (int)Slider_Volumne.Value; } // private Brush _OldButtonBrush = SystemColors.ControlBrush; private void Button_Save_Click(object xSender, RoutedEventArgs e) { _OldButtonBrush = Button_Save.Background; Button_Save.Background = Brushes.Salmon; Microsoft.Win32.SaveFileDialog lDialog = new Microsoft.Win32.SaveFileDialog(); string lPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); lDialog.InitialDirectory = lPath; lDialog.FileOk += FileDialog_FileOk; lDialog.Filter = "All Files|*.*|WAV (*.wav)|*.wav"; lDialog.FilterIndex = 2; lDialog.ShowDialog(); } // void FileDialog_FileOk(object xSender, System.ComponentModel.CancelEventArgs e) { Microsoft.Win32.SaveFileDialog lDialog = xSender as Microsoft.Win32.SaveFileDialog; if (lDialog == null) return; if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(() => FileDialog_FileOk(xSender, e)); return; } try { string lPathAndFile = lDialog.FileName; _SpeechSynthesizer.SetOutputToWaveFile(lPathAndFile); _SpeechSynthesizer.SpeakCompleted += SpeechSynthesizer_SpeakCompleted; SayIt(TextBox_Clipboard.Text); Button_Save.Background = _OldButtonBrush; } catch (Exception ex) { MessageBox.Show(ex.Message); } } // void SpeechSynthesizer_SpeakCompleted(object sender, SpeakCompletedEventArgs e) { _SpeechSynthesizer.SetOutputToDefaultAudioDevice(); _SpeechSynthesizer.SpeakCompleted -= SpeechSynthesizer_SpeakCompleted; } // } // class } // namespace
Timer Finetuning, Each Microsecond Counts
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.27SYSTEM 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 64211THREAD 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 13203LOOP 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 13037SpinWait 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 microsecondsMICRO 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)
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)
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.
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
Basic Thread
static private void myThread() { Thread.Sleep(2000); Console.WriteLine("hello world!"); } // static void Main(string[] args) { Thread t = new Thread(new ThreadStart(myThread)); t.IsBackground = true; t.Name = "MyBackgroundThread"; t.Priority = ThreadPriority.Normal; Console.WriteLine("starting new thread"); t.Start(); Console.WriteLine("waiting for started thread to finish"); t.Join(); Console.WriteLine("press return to exit the program"); Console.ReadLine(); } //
Nearly all PCs these days have multiple cores and/or CPUs. There is a speed benefit using them simultaneously. Windows can run applications independently. You can eg. run Notpad and Paint in parallel. Each application runs as a process. And each process can have many threads. A thread is more or less some code that is executed for a certain time period. Windows decides on its own when to pause a thread and switch to another thread. This is called context switching.
To run a simple thread we need the “System.Threading” namespace. You declare it with “using System.Threading;”.
The output console is a static and thread safe class. You neither have to instantiate the class nor synchronise the output. This is quite helpful, otherwise the example program would have to be a bit more complex. You would not be able to get access to the console from two different threads without risking deadlocks.
It is good practice to define the thread priority. That way Windows knows how important your code is and how much time it should assign for its execution. In a stock trading system you would eg. assign a high priority to the trade decision thread and a low priority to any screen output.
The join method is called on the main thread to let it wait until our newly generated thread terminates. You cannot restart a thread once it has terminated. In our example we started a background thread by setting t.IsBackground to true. All background threads (and the application) automatically stop as soon as all Foreground threads have terminated. Thus Foreground threads need to handle the shut down process, otherwise the application keeps on running unintentionally.
Some threads need parameters. Have a look at the following code. It uses a class to pass two parameters to the new thread.
private class parameters { public double d; public string s; }// static private void myThread(object xParameters) { parameters p = xParameters as parameters; Console.WriteLine("your parameters are " + p.d + " " + p.s); } // static void Main(string[] args) { Thread t = new Thread(new ParameterizedThreadStart(myThread)); t.IsBackground = true; t.Name = "MyBackgroundThread"; t.Priority = ThreadPriority.Normal; parameters p = new parameters{d = 2.5, s = "meters"}; t.Start(p); t.Join(); Console.WriteLine("press return to exit the program"); Console.ReadLine(); } //