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
WPF TimeLine Custom Control
This is a custom WPF control and not a UserForm or a standard control with some extra properties in XAML. It is a proper class, which you would usually place in a library.
All Controls inherit from the FrameworkElement class. Some inherit from the Control Class, which is derived from the FrameworkElement class. In this case here I am deriving our custom control from the Canvas class, which in turn is derived from the Panel class and inherently FrameworkElement class. Once again I am not re-inventing the wheel and use existing functionality. Therefore the code remains pretty short. I am basically overriding the OnRender() event of the Canvas class.
This effects the design-time and run-time, which means you can see immediate results while you add the control in the XAML/Blend editor and of course when your C# application is running.
Just in case you are interested in further reading for design-time enhancements: WpfTutorial
The code in the MainWindow is not required. This is just a demonstration on how to change the display at run-time. A timer is used to make the change obvious. The code execution would be too fast otherwise. You wouldn’t see the first TimeLine graph, just the second. The method SetTimeSeries() triggers the change by invalidating the Canvas control (method InvalidateVisual()).
You can see a similar initialization in the TimeLineControl constructor. This data can be seen at design-time. You could also code that part in the XAML code, but this is not the purpose here. The control should not need any additional XAML in its ‘distributable’ version. Implement the TimeLineControl as it is. That is the beauty of overriding OnRender() rather than using WPF templates.
The inner control paddings and other settings were hard-coded. You can change these. Hard-coding made the example easier to understand – no redundant code to confuse you. Feel free to replace this part with flexible user settings or calculated values.
<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="TimeLine.MainWindow" xmlns:local="clr-namespace:TimeLine" Title="TimeLineControl" Height="130" Width="525"> <DockPanel> <local:TimeLineControl x:Name="MyTimeLineControl" /> </DockPanel> </Window>
using System; using System.Collections.Generic; using System.Timers; using System.Windows; using System.Windows.Documents; namespace TimeLine { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Timer lTimer = new Timer(3000.0); lTimer.Elapsed += Timer_Elapsed; lTimer.Start(); } // constructor void Timer_Elapsed(object xSender, ElapsedEventArgs e) { Timer lTimer = xSender as Timer; if (lTimer == null) return; lTimer.Stop(); // demo: how to change the TimeLine List<TimeEvent> lList = new List<TimeEvent>(); AddEvent(lList, new DateTime(2015, 03, 01), ""); AddEvent(lList, new DateTime(2015, 03, 06), "exD Brown-Forman Corp"); AddEvent(lList, new DateTime(2015, 03, 10), "exD UniFirst Corp"); AddEvent(lList, new DateTime(2015, 03, 11), "exD Worthington Industries Inc"); AddEvent(lList, new DateTime(2015, 03, 12), "exD Garmin Ltd"); AddEvent(lList, new DateTime(2015, 03, 18), "exD Republic Bank Corp"); AddEvent(lList, new DateTime(2015, 03, 23), "exD STMicroelectronics NV"); AddEvent(lList, new DateTime(2015, 03, 31), ""); MyTimeLineControl.SetTimeSeries(lList); } // constructor private void AddEvent(List<TimeEvent> xList, DateTime xDate, string xText) { TimeEvent lEvent = new TimeEvent(); lEvent.Date = xDate; lEvent.TextRed = xDate.ToString("dd MMMyy"); lEvent.TextBlack = xText; xList.Add(lEvent); } // } // class } // namespace
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Shapes; namespace TimeLine { public class TimeLineControl : Canvas { private IEnumerable<TimeEvent> _TimeSeries; public TimeLineControl() { Background = Brushes.White; List<TimeEvent> lList = new List<TimeEvent>(); lList.Add(new TimeEvent { Date = DateTime.Today, TextRed = ":(", TextBlack = "Today" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(1), TextRed = "", TextBlack = "Tomorrow" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(7), TextRed = "", TextBlack = "Next week" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddDays(14), TextRed = "", TextBlack = "Fortnight" }); lList.Add(new TimeEvent { Date = DateTime.Today.AddMonths(1), TextRed = ":)", TextBlack = "NextMonth" }); _TimeSeries = lList; } // constructor public void SetTimeSeries(IEnumerable<TimeEvent> xList) { if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(() => SetTimeSeries(xList)); return; } _TimeSeries = xList; InvalidateVisual(); } // // some hardcoding to keep the example code simple const double cPaddingLeft = 15.0; const double cPaddingRight = 50.0; const double cPaddingBottom = 17.0; const double cMarkerLength = 10.0; const double cTextOffset = 5.0; // distance to rotation point const double cTextSpace = 7.0; // distance between the red and the black text const double cAngle = 45.0; // rotation angle (0.0 <= cAngle <= 180.0) protected override void OnRender(DrawingContext xDrawingContext) { base.OnRender(xDrawingContext); double lLeft = cPaddingLeft; double lRight = ActualWidth - cPaddingRight; double lBottom = ActualHeight - cPaddingBottom; double lWidth = ActualWidth - cPaddingLeft - cPaddingRight; Point p1 = new Point(lLeft, lBottom); Point p2 = new Point(lRight, lBottom); // draw the X-Axis Pen lPen = new Pen(Brushes.Black, 3.0); lPen.DashStyle = DashStyles.Solid; xDrawingContext.DrawLine(lPen, p1, p2); // determine range DateTime lMin = _TimeSeries.Min(x => x.Date); DateTime lMax = _TimeSeries.Max(x => x.Date); double lDateRange = lMax.Subtract(lMin).TotalDays; foreach (TimeEvent t in _TimeSeries) { double lRelativeX = t.Date.Subtract(lMin).TotalDays / lDateRange; double lAbsoluteX = lRelativeX * lWidth + lLeft; // convert to canvas coordinates // draw the X-Axis marker p1 = new Point(lAbsoluteX, lBottom); p2 = new Point(lAbsoluteX, lBottom - cMarkerLength); xDrawingContext.DrawLine(lPen, p1, p2); // write the text with a 45 degrees angle Point lRotationCenter = p2; double lTextWidth = DrawText(xDrawingContext, t.TextRed, lRotationCenter, cTextOffset, Brushes.Red); // red text DrawText(xDrawingContext, t.TextBlack, lRotationCenter, lTextWidth + cTextOffset + cTextSpace, Brushes.Black); // black text } } // /// <returns>the width of the text</returns> private double DrawText(DrawingContext xDrawingContext, string xText, Point xPoint, double xOffset, SolidColorBrush xBrush) { Typeface lTypeface = new Typeface("Arial"); CultureInfo lCultureInfo = CultureInfo.CurrentCulture; FormattedText lText = new FormattedText(xText, lCultureInfo, FlowDirection.LeftToRight, lTypeface, 10.0, xBrush); RotateTransform lRotateTransform = new RotateTransform(-cAngle, xPoint.X, xPoint.Y); xDrawingContext.PushTransform(lRotateTransform); xDrawingContext.DrawText(lText, new Point(xPoint.X + xOffset, xPoint.Y - lText.Height / 2.0)); xDrawingContext.Pop(); //return new Point(xPoint.X + lText.Height / 2.0 * Math.Sin(cAngle) + lText.Width * Math.Cos(cAngle) , xPoint.Y - lText.Width * Math.Sin(cAngle)); return lText.Width; } // private void DrawLine(double x1, double x2, double y1, double y2) { Line lLine = new Line(); lLine.Stroke = Brushes.LightSteelBlue; lLine.X1 = x1; lLine.X2 = x2; lLine.Y1 = y1; lLine.Y2 = y2; lLine.HorizontalAlignment = HorizontalAlignment.Left; lLine.VerticalAlignment = VerticalAlignment.Center; lLine.StrokeThickness = 2; Children.Add(lLine); } // } // class } // namespace
using System; namespace TimeLine { public class TimeEvent { public DateTime Date; public string TextRed; public string TextBlack; } // 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
Routed Events (part 2)
Referring back to Routed Events (to part 1), let’s have a closer look at this part of the example source code:
// bubbling private void MyMouseUp(object sender, MouseButtonEventArgs e) { FrameworkElement lElement = sender as FrameworkElement; string lAppend = Environment.NewLine; if (sender is Window) lAppend += Environment.NewLine; Results.Text += e.RoutedEvent.RoutingStrategy.ToString() + ": " + lElement.ToString() + lAppend; e.Handled = false; Results.ScrollToEnd(); } //
Suppressing Events
e.Handled allows you to halt the event routing process. Set this boolean to true and the event stops traveling any further. A small change demonstrates the altered behavior:
// bubbling private void MyMouseUp(object sender, MouseButtonEventArgs e) { FrameworkElement lElement = sender as FrameworkElement; string lAppend = Environment.NewLine; if (sender is Window) lAppend += Environment.NewLine; Results.Text += e.RoutedEvent.RoutingStrategy.ToString() + ": " + lElement.ToString() + lAppend; e.Handled = (e.ChangedButton == MouseButton.Right); Results.ScrollToEnd(); } //
If you use the right MouseButton now, the bubbling routing event process stops. The same applies to the tunneling process when you change the MyPreviewMouseUp() method accordingly.
Raising Suppressed Events
You can avoid the suppression of Routed Events. This cannot be done through XAML. Use the AddHandler() method instead. An overload accepts a boolean for its third parameter. Set this one to true and you will receive events even if the e.Handled flag was set to true.
Let’s slightly change our example source code to:
<Window x:Class="DemoApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:app="clr-namespace:DemoApp" Title="MainWindow" Height="500" Width="630" Name="MyWindow" PreviewMouseUp="MyPreviewMouseUp"> ...
... public MainWindow() { InitializeComponent(); List<Data> lItems = new List<Data>() { new Data() {Name = "Otto", LastName = "Waalkes"}, new Data() {Name = "Heinz", LastName = "Rühmann"}, new Data() {Name = "Michael", LastName = "Herbig"}, new Data() {Name = "Sky", LastName = "du Mont"}, new Data() {Name = "Dieter", LastName = "Hallervorden"}, new Data() {Name = "Diether", LastName = "Krebs"}, new Data() {Name = "Helga", LastName = "Feddersen"}, new Data() {Name = "Herbert", LastName = "Grönemeyer"}, }; MyListView.ItemsSource = lItems; MyWindow.AddHandler(UIElement.MouseUpEvent, new MouseButtonEventHandler(MyMouseUp), true); } // ...
Et voilà! The Routed Event gets executed despite the set e.Handled flag.
Attached Events
The Click event is defined in the ButtonBase class. It is a kind of combination of a Button press and release. But how can you use the bubbling behavior on a higher level like eg. a Grid that does not derive from the ButtonBase class? Attached events enable you to add event handlers to arbitrary elements, which do not define or inherit these.
Let’s add a Click event to the window level by adding Button.Click=”MyClick”:
<Window x:Class="DemoApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:app="clr-namespace:DemoApp" Title="MainWindow" Height="500" Width="630" Name="MyWindow" PreviewMouseUp="MyPreviewMouseUp" Button.Click="MyClick" > ...
private void MyClick(object sender, RoutedEventArgs e) { MessageBox.Show("Click received!"); } //
The program does not raise any Click events. We did even override the Click event of our TripleClickButton class. You won’t see a lot. But have a look at the two scroll bars. The scroll bar background (not the scroll bar itself) raises click events. As we are on the window level, we now receive these unexpected events. Indeed, this is a good example. A click of the scroll bar background bubbles through the hierarchy and finally raises the attached Click event on the window level.
Don’t forget to analyse the e.Source of your event parameter. You need to filter out the right Click event.
Style EventSetter
While Property setters are most common in Styles, EventSetters are rarely seen. They can be used for more complex problems. The simple ones should be solved by using Style.Triggers. Let’s say you want to change the color of a TextBlock when entering or leaving the area with the mouse cursor.
... <Window.Resources> <Style x:Key="ChangeBackgroundColor" TargetType="TextBlock"> <EventSetter Event="TextBlock.MouseEnter" Handler="ChangeBackgroundColorOnMouseEnter" /> // direct event <EventSetter Event="TextBlock.MouseLeave" Handler="ChangeBackgroundColorOnMouseLeave" /> // direct event </Style> </Window.Resources> ... <TextBlock Text="LastName: " Grid.Column="2" VerticalAlignment="Center" FontSize="16" Style="{StaticResource ChangeBackgroundColor}"/> ...
private void ChangeBackgroundColorOnMouseEnter(object sender, MouseEventArgs e) { ((TextBlock)sender).Background = Brushes.Red; } private void ChangeBackgroundColorOnMouseLeave(object sender, MouseEventArgs e) { ((TextBlock)sender).Background = null; }
This example could be simplified. No C# code required:
... <Window.Resources> <Style x:Key="ChangeBackgroundColor" TargetType="TextBlock"> <Style.Triggers> <Trigger Property="TextBlock.IsMouseOver" Value="True"> <Setter Property="TextBlock.Background" Value="Red" /> </Trigger> </Style.Triggers> </Style> </Window.Resources> ... <TextBlock Text="LastName: " Grid.Column="2" VerticalAlignment="Center" FontSize="16" Style="{StaticResource ChangeBackgroundColor}"/> ...
I see the need for further explanations on Trigger types. I have just added a reminder on my To-Do-List.
But for now a simple list must suffice:
- Trigger: Simplest trigger form. Reacts on DependencyProperty changes and then uses setters to change styles.
- MultiTrigger: Combines multiple Triggers. All conditions must be met.
- DataTrigger: Reacts on changes in bound data.
- MultiDataTrigger: Combines multiple DataTriggers. All conditions must be met.
- EventTrigger: Reacts on events. Used for animations.
In a nutshell: There are three trigger types. They use dependency properties, routed events or data binding.
WPF Datagrid formatting (part 2, advanced)
We stick to the previous DataGrid example and enhance it now.
The improvements/additions are:
- Cells are vertically centered now.
- Copy/paste includes the header text.
ClipboardCopyMode="IncludeHeader"
- Templates cannot be copied/pasted. The DataGrid does not know what property it has to read. Therefore a ClipboardContentBinding was added.
ClipboardContentBinding="{Binding Birthday}
- A yellow smiley is drawn on a Canvas with ellipses and a Bézier curve.
- The birthday string is formatted.
- The DataGrid rows use alternating colors.
- CheckBoxes are centered in the cells.
- A bit closer to hardcore: DatePicker
The method to remove all borders requires slightly more know-how. The required information was taken from my earlier post WPF Control Templates (part 1). Also the background color of the DatePickerTextBox is made transparent. This is done without defining a new template for the DatePicker.The XAML definition
<DatePicker SelectedDate="{Binding Birthday, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" BorderThickness="0" Loaded="DataGrid_DatePicker_Loaded" />
calls:
private void DataGrid_DatePicker_Loaded(object sender, RoutedEventArgs e) {...}
which in turn calls:
private static void RemoveBorders(DependencyObject xDependencyObject) {...}
- RowHeaders were added. On the internet you can find a lot of ToggleButton examples. You face the same code roots over and over again. To avoid coming up with a similar example I used a button with a +/- sign instead. This way you can easily change the code and replace the text by custom images.
- My advice here is: Play with the FrozenColumnCount property. I am sure you will need it someday.
- This example uses more templates than the last one.
- RowDetailsTemplate was added. This enables expanding DataGrid rows to eg. show or enter additional information.
- UpdateSourceTrigger makes sure you can see DataGridCell changes immediately on the RowDetailsTemplate.
To achieve this, the class Person needs to inherit INotifyPropertyChanged.
<Window x:Class="WpfDatagrid.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Language="en-GB" Loaded="Window_Loaded" Closed="Window_Closed" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <!-- DataGrid: header style --> <Style TargetType="{x:Type DataGridColumnHeader}"> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="SeparatorBrush" Value="WhiteSmoke" /> <Setter Property="FontWeight" Value="Bold" /> </Style> <!--DataGrid: vertical/horizontal text alignment --> <Style x:Key="AlignRight" TargetType="{x:Type TextBlock}"> <Setter Property="HorizontalAlignment" Value="Right" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <Style x:Key="AlignLeft" TargetType="{x:Type TextBlock}"> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="VerticalAlignment" Value="Center" /> </Style> <!--DataGrid: center the CheckBox --> <Style x:Key="AlignCheckBox" TargetType="{x:Type DataGridCell}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGridCell}"> <Grid Background="{TemplateBinding Background}"> <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Center" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> <!-- DataGrid: template for expandable area --> <DataTemplate x:Key="TemplateRowDetails"> <DockPanel> <Canvas DockPanel.Dock="Left" Width="60"> <Canvas.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FF061246" Offset="0.497"/> <GradientStop Color="#FF7F7FC9" Offset="1"/> </LinearGradientBrush> </Canvas.Background> <Ellipse Fill="Yellow" Height="50" Width="50" StrokeThickness="2" Stroke="Black" Canvas.Left="5" Canvas.Top="5" /> <Ellipse Fill="Black" Height="12" Width="8" Canvas.Left="17" Canvas.Top="20" /> <Ellipse Fill="Black" Height="12" Width="8" Canvas.Left="37" Canvas.Top="20" /> <Path Stroke="Black" StrokeThickness="3"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigureCollection> <PathFigure StartPoint="15,37"> <PathFigure.Segments> <PathSegmentCollection> <QuadraticBezierSegment Point1="30,52" Point2="45,37" /> </PathSegmentCollection> </PathFigure.Segments> </PathFigure> </PathFigureCollection> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> </Canvas> <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold" Text="{Binding FirstName}" /> <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold" Text="{Binding LastName}" /> <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold" Text="{Binding Birthday, StringFormat={}{0:dd MMMM yyyy}}" /> <TextBlock DockPanel.Dock="Top" FontSize="12" FontWeight="Bold" Text="{Binding Homepage}" /> </DockPanel> </DataTemplate> </Window.Resources> <Grid> <!-- advice: play with the property FrozenColumnCount --> <DataGrid AlternatingRowBackground="PeachPuff" AutoGenerateColumns="False" ItemsSource="{Binding}" CanUserAddRows="False" CanUserReorderColumns="True" CanUserResizeColumns="True" CanUserResizeRows="False" SelectionUnit="Cell" SelectionMode="Extended" ClipboardCopyMode="IncludeHeader" RowDetailsTemplate="{StaticResource TemplateRowDetails}" ColumnHeaderHeight="{Binding RowHeight, RelativeSource={RelativeSource Self}}"> <DataGrid.RowStyle> <Style TargetType="DataGridRow"> <Setter Property="DetailsVisibility" Value="Collapsed" /> </Style> </DataGrid.RowStyle> <!-- DataGrid: RowHeader --> <DataGrid.RowHeaderTemplate> <DataTemplate> <Button Click="DataGridRowHeader_Button_Click" Cursor="Hand" HorizontalAlignment="Center" > <Button.Style> <Style TargetType="Button"> <Style.Setters> <Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="Content" Value="+" /> <Setter Property="FontStretch" Value="UltraExpanded" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="Height" Value="20" /> <Setter Property= "Width" Value="20" /> <!-- <Setter Property="Width" Value="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}" /> --> </Style.Setters> <Style.Triggers> <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type DataGridRow}},Path=DetailsVisibility}" Value="Visible"> <Setter Property="Background" Value="Salmon" /> <Setter Property="Content" Value="-" /> <Setter Property="Height" Value="86" /> </DataTrigger> </Style.Triggers> </Style> </Button.Style> </Button> </DataTemplate> </DataGrid.RowHeaderTemplate> <!-- DataGrid: Row color when selected --> <DataGrid.CellStyle> <Style> <Style.Setters> <Setter Property="DataGridCell.VerticalContentAlignment" Value="Center" /> </Style.Setters> <Style.Triggers> <Trigger Property="DataGridCell.IsSelected" Value="True"> <Setter Property="DataGridCell.Background" Value="SteelBlue" /> </Trigger> </Style.Triggers> </Style> </DataGrid.CellStyle> <DataGrid.Columns> <!-- Column: Alive --> <DataGridCheckBoxColumn Header="Alive" Binding="{Binding Alive, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" CellStyle="{StaticResource AlignCheckBox}" /> <!-- Column: Name --> <DataGridTextColumn Header="Name" Binding="{Binding FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ElementStyle="{StaticResource AlignLeft}" /> <!-- Column: LastName --> <DataGridTextColumn Header="LastName" Binding="{Binding LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ElementStyle="{StaticResource AlignLeft}" /> <!-- Column: Birthday --> <DataGridTemplateColumn Header="Birthday" SortMemberPath="Birthday.Day" ClipboardContentBinding="{Binding Birthday}"> <DataGridTemplateColumn.CellTemplate> <DataTemplate> <DatePicker SelectedDate="{Binding Birthday, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" BorderThickness="0" Loaded="DataGrid_DatePicker_Loaded" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <!-- Column: Age --> <DataGridTextColumn Header="Age" Binding="{Binding Age, StringFormat=N2}" ElementStyle="{StaticResource AlignRight}" IsReadOnly="True" /> <!-- Column: Homepage --> <DataGridHyperlinkColumn Header="Homepage" Binding="{Binding Homepage}" IsReadOnly="True"> <DataGridHyperlinkColumn.ElementStyle> <Style> <EventSetter Event="Hyperlink.Click" Handler="Hyperlink_Clicked"/> <Setter Property="TextBlock.HorizontalAlignment" Value="Left" /> <Setter Property="TextBlock.VerticalAlignment" Value="Center" /> </Style> </DataGridHyperlinkColumn.ElementStyle> </DataGridHyperlinkColumn> </DataGrid.Columns> </DataGrid> </Grid> </Window>
using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Media; namespace WpfDatagrid { public partial class MainWindow : Window { public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string xName) { PropertyChangedEventHandler h = PropertyChanged; if (h == null) return; h(this, new PropertyChangedEventArgs(xName)); } // private bool _Alive; public bool Alive { get { return _Alive; } set { _Alive = value; OnPropertyChanged("Alive"); } } private string _FirstName; public string FirstName { get { return _FirstName; } set { _FirstName = value; OnPropertyChanged("FirstName"); } } private string _LastName; public string LastName { get { return _LastName; } set { _LastName = value; OnPropertyChanged("LastName"); } } public double Age { get { return DateTime.Now.Subtract(Birthday).TotalDays / 365; } } public string Homepage { get; set; } private DateTime _Birthday; public DateTime Birthday { get { return _Birthday; } set { _Birthday = value; OnPropertyChanged("Birthday"); } } } // class public MainWindow() { InitializeComponent(); } // // set the window DataContext private void Window_Loaded(object sender, RoutedEventArgs e) { List<Person> lPersons = new List<Person>(); lPersons.Add(new Person() { FirstName = "Liza", LastName = "Minnelli", Birthday = new DateTime(1946, 03, 12), Alive = true, Homepage = "www.officiallizaminnelli.com" }); lPersons.Add(new Person() { FirstName = "Bastian", LastName = "Ohta", Birthday = new DateTime(1975, 03, 13), Alive = true, Homepage = "www.ohta.de" }); lPersons.Add(new Person() { FirstName = "Albert", LastName = "Einstein", Birthday = new DateTime(1879, 03, 14), Alive = false, Homepage = "www.alberteinsteinsite.com" }); lPersons.Add(new Person() { FirstName = "Coenraad", LastName = "van Houten", Birthday = new DateTime(1801, 03, 15), Alive = false, Homepage = "www.vanhoutendrinks.com" }); lPersons.Add(new Person() { FirstName = "Andrew", LastName = "Miller-Jones", Birthday = new DateTime(1910, 03, 16), Alive = false, Homepage = "dead as a Dodo" }); lPersons.Add(new Person() { FirstName = "Gottlieb", LastName = "Daimler", Birthday = new DateTime(1834, 03, 17), Alive = false, Homepage = "www.daimler.com" }); lPersons.Add(new Person() { FirstName = "Rudolf", LastName = "Diesel", Birthday = new DateTime(1858, 03, 18), Alive = false, Homepage = "http://en.wikipedia.org/wiki/Rudolf_Diesel" }); DataContext = lPersons; } // // exit the application private void Window_Closed(object sender, EventArgs e) { Application.Current.Shutdown(0); } // // open the hyperlink in a browser private void Hyperlink_Clicked(object sender, RoutedEventArgs e) { try { Hyperlink lHyperlink = e.OriginalSource as Hyperlink; string lUri = lHyperlink.NavigateUri.OriginalString; Process.Start(lUri); } catch (Exception ex) { MessageBox.Show(ex.Message); } } // // find the correct DataGridRow and set the DetailsVisibility private void DataGridRowHeader_Button_Click(object sender, RoutedEventArgs e) { DependencyObject lDependencyObject = e.OriginalSource as DependencyObject; //Button lButton = lDependencyObject as Button; //if (lButton == null) return; while (!(lDependencyObject is DataGridRow) && lDependencyObject != null) lDependencyObject = VisualTreeHelper.GetParent(lDependencyObject); DataGridRow lRow = lDependencyObject as DataGridRow; if (lRow == null) return; //lRow.IsSelected = (lRow.DetailsVisibility != Visibility.Visible); lRow.DetailsVisibility = lRow.DetailsVisibility == System.Windows.Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed; Console.WriteLine(lRow.ActualHeight); } // private void DataGrid_DatePicker_Loaded(object sender, RoutedEventArgs e) { // get the DatePicker control DatePicker lDatePicker = sender as DatePicker; lDatePicker.VerticalContentAlignment = System.Windows.VerticalAlignment.Center; // find the inner textbox and adjust the Background colour DatePickerTextBox lInnerTextBox = lDatePicker.Template.FindName("PART_TextBox", lDatePicker) as DatePickerTextBox; lInnerTextBox.Background = Brushes.Transparent; lInnerTextBox.VerticalContentAlignment = System.Windows.VerticalAlignment.Center; lInnerTextBox.Height = lDatePicker.ActualHeight - 2; // remove watermark ContentControl lWatermark = lInnerTextBox.Template.FindName("PART_Watermark", lInnerTextBox) as ContentControl; lWatermark.IsHitTestVisible = false; lWatermark.Focusable = false; lWatermark.Visibility = System.Windows.Visibility.Collapsed; lWatermark.Opacity = 0; // just as demo ContentControl lContentHost = lInnerTextBox.Template.FindName("PART_ContentHost", lInnerTextBox) as ContentControl; // remove ugly borders RemoveBorders(lInnerTextBox); // hardcore 🙂 } // private static void RemoveBorders(DependencyObject xDependencyObject) { for (int i = 0, n = VisualTreeHelper.GetChildrenCount(xDependencyObject); i < n; i++) { DependencyObject lDependencyObject = VisualTreeHelper.GetChild(xDependencyObject, i); RemoveBorders(lDependencyObject); Border lBorder = lDependencyObject as Border; if (lBorder == null) continue; lBorder.BorderBrush = Brushes.Transparent; } } // } // class } // namespace