Delay's Blog is the blog of David Anson, a Microsoft developer who works with the Silverlight, WPF, Windows Phone, and web platforms.
http://dlaa.me/
@DavidAns
While answering a support forum question I'd seen a couple of times before, I figured it would be helpful to write a post showing how to make simple styling changes to charts created by the Charting controls that come with the Silverlight Toolkit. Note that I said simple changes - if you want to make more dramatic changes, you should go read some of the excellent tutorials Pete Brown has written on the topic. Links to Pete's posts (and other interesting posts) can be found on my latest Charting links post.
The sample application we'll be working with here shows off four scenarios and looks like this:
Simple Color Change
In this scenario we have a basic Chart with a ColumnSeries and want to change the color to purple. As you'd expect, this is quite easy to do: we provide a custom Style that sets the Background color to purple and we're done!
Chart
ColumnSeries
Style
Background
It's worth pointing out that we could add more Setters to this Style to customize things further - but I promised I'd keep this simple, so we won't do that right now. :)
Setter
<charting:Chart Title="Simple Color Change"> <charting:ColumnSeries ItemsSource="{Binding}" DependentValueBinding="{Binding Length}" IndependentValueBinding="{Binding}"> <charting:ColumnSeries.DataPointStyle> <Style TargetType="charting:ColumnDataPoint"> <Setter Property="Background" Value="Purple"/> </Style> </charting:ColumnSeries.DataPointStyle> </charting:ColumnSeries> </charting:Chart>
Custom ToolTip (Column)
For this example, the setup is the same as last time except that now we want to change the default ToolTip that appears when the user hovers over any of the columns. As with just about every other visual default, the standard ToolTip is part of the ColumnDataPoint's default Template. So in order to customize it we start with a copy of that Template and modify it to suit our needs. Blend makes this easy, but I'm most comfortable in Visual Studio, so what we'll do here is go to the source code for ColumnDataPoint.xaml and copy the Style there to the Application.Resources section of our App.xaml.
ToolTip
ColumnDataPoint
Template
Application.Resources
App.xaml
Aside: You can also use my handy-dandy SilverlightDefaultStyleBrowser for this. What's more, SilverlightDefaultStyleBrowser works even when you don't have access to source code for the control you're styling, so it's something to keep in mind for those occasions when Blend isn't readily available. :)
Copying done, we can tweak the ToolTip to include a custom message as follows:
<ToolTipService.ToolTip> <StackPanel> <ContentControl Content="Custom ToolTip" FontWeight="Bold"/> <ContentControl Content="{TemplateBinding FormattedDependentValue}"/> </StackPanel> </ToolTipService.ToolTip>
After that, and it's simply a matter of assigning our customized Style/Template to the DataPointStyle property of the ColumnSeries:
DataPointStyle
<charting:Chart Title="Custom ToolTip"> <charting:ColumnSeries ItemsSource="{Binding}" DependentValueBinding="{Binding Length}" IndependentValueBinding="{Binding}" DataPointStyle="{StaticResource MyColumnDataPointStyle}"/> </charting:Chart>
Simple Palette Change
What we've done so far will work for all of the current series except for PieSeries which is special because each of its PieDataPoints gets a unique Style. In other words, there's no DataPointStyle property on PieSeries because one value just isn't enough! Therefore, PieSeries exposes a StylePalette property just like Chart does and we can use that to provide a collection of Styles for the pie slices. (Note that we can provide as many or as few as we want; PieSeries will start at the beginning and automatically loop through the collection as necessary.)
PieSeries
PieDataPoint
StylePalette
Styles
In this case, we know our data has exactly four items, so we'll provide exactly four custom Styles to set the colors we want. Other than using a list this time around, it's just like the first example we saw:
<charting:Chart Title="Simple Palette Change"> <charting:PieSeries ItemsSource="{Binding}" DependentValueBinding="{Binding Length}" IndependentValueBinding="{Binding}"> <charting:PieSeries.StylePalette> <datavis:StylePalette> <Style TargetType="charting:PieDataPoint"> <Setter Property="Background" Value="Red"/> </Style> <Style TargetType="charting:PieDataPoint"> <Setter Property="Background" Value="Orange"/> </Style> <Style TargetType="charting:PieDataPoint"> <Setter Property="Background" Value="Green"/> </Style> <Style TargetType="charting:PieDataPoint"> <Setter Property="Background" Value="Blue"/> </Style> </datavis:StylePalette> </charting:PieSeries.StylePalette> </charting:PieSeries> </charting:Chart>
Custom ToolTip (Pie)
Finally, let's customize the ToolTip for the slices of a PieSeries. Like before, we'll start by copying the default Style/Template from the source code for PieDataPoint.xaml and then customize the ToolTip found within:
<ToolTipService.ToolTip> <StackPanel> <ContentControl Content="Custom ToolTip" FontWeight="Bold"/> <ContentControl Content="{TemplateBinding FormattedDependentValue}"/> <ContentControl Content="{TemplateBinding FormattedRatio}"/> </StackPanel> </ToolTipService.ToolTip>
Because we want our PieSeries to use all the same colors as the default, the next step is to copy the default StylePalette from the code for Chart.xaml and add a single Setter for the Template property of each of the Styles within. All of which point to the one Template we just customized, so if we make any changes in the future there's exactly one place we need to touch and our changes automatically shows up everywhere they should:
<datavis:StylePalette x:Key="MyStylePalette"> <!--Blue--> <Style TargetType="Control"> <Setter Property="Template" Value="{StaticResource MyPieDataPointTemplate}"/> <Setter Property="Background"> <Setter.Value> ... </Setter.Value> </Setter> </Style> <!--Red--> <Style TargetType="Control"> <Setter Property="Template" Value="{StaticResource MyPieDataPointTemplate}"/> <Setter Property="Background"> <Setter.Value> ... </Setter.Value> </Setter> </Style> ...
With that out of the way, all that remains is to use our customized StylePalette by assigning it to the StylePalette property of PieSeries:
<charting:Chart Title="Custom ToolTip"> <charting:PieSeries ItemsSource="{Binding}" DependentValueBinding="{Binding Length}" IndependentValueBinding="{Binding}" StylePalette="{StaticResource MyStylePalette}"> </charting:PieSeries> </charting:Chart>
Done!
If you've gotten this far, I hope that you've gained at least a somewhat better understanding of how to perform some basic style changes to the Toolkit's Charting controls. We've really only scratched the surface, though, so I encourage interested readers to have a look at some of the other charting links for more details, ideas, and inspiration!
[Please click here to download the source code for the sample application.]
Earlier this week I wrote about the "app building" exercise my team conducted and posted my sample application, a simple organizational hierarchy viewer using many of the controls in the Silverlight Toolkit. One of the surprises I had during the process of building this application was that Silverlight (version 2 as well as the Beta for version 3) doesn't support the scenario of providing a Binding in the Value of a Setter. I bumped into this when I was trying to follow one of the "best practices" for TreeView manipulation - but I soon realized the problem has much broader reach.
First, a bit about why this is interesting at all. :) Because of the way TreeView works on WPF and Silverlight, it turns out that the most elegant way of manipulating the nodes (for example, to expand all the nodes in a tree) is to do so by manipulating your own classes to which the TreeViewItem's IsExpanded property is bound. Josh Smith does a great job explaining why in this article, so I won't spend more time on that here. However, as Bea Stollnitz explains near the bottom of this post (and as I mention above), the XAML-based Setter/Value/Binding approach doesn't work on Silverlight.
TreeView
Value
Binding
In her post, Beatriz outlines a very reasonable workaround for this problem which is to subclass TreeView and TreeViewItem in order to override GetContainerForItemOverride and hook up the desired Bindings there. However, there are two drawbacks with this approach that I hoped to be able to improve upon: 1. It's limited to ItemsControl subclasses (because other controls don't have GetContainerForItemOverride) and 2. it moves design concerns into code (where designers can't see or change them).
TreeViewItem
GetContainerForItemOverride
As part of my app building work, I came up with a one-off way of avoiding the ItemsControl coupling, but it wasn't broadly useful in its original form. Fortunately, in the process of extracting that code out in generalizing it for this post, I realized how I could avoid the second drawback as well and accomplish the goal without needing any code at all - it's all XAML, all the time! [Yes, designers, I [heart] you. :) ]
ItemsControl
The trick is to use a Setter to set a special attached DependencyProperty with a Value that specifies a special object which identifies the DependencyProperty and Binding to set. It's that easy! Well, okay, I have to do a bit of work behind the scenes to make this all hang together, but it does work - and what's more it even works for attached properties!
DependencyProperty
Here's an example of SetterValueBindingHelper in action:
SetterValueBindingHelper
First, the relevant XAML for the TreeViewItem:
<Style TargetType="controls:TreeViewItem"> <!-- WPF syntax: <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/> --> <Setter Property="local:SetterValueBindingHelper.PropertyBinding"> <Setter.Value> <local:SetterValueBindingHelper Property="IsExpanded" Binding="{Binding IsExpanded}"/> </Setter.Value> </Setter> </Style>
Yes, things end up being a little bit more verbose than they are on WPF, but if you squint hard enough the syntax is quite similar. Even better, it's something that someone who hasn't read this post should be able to figure out on their own fairly easily.
Here's the XAML for the top Button:
Button
<Style TargetType="Button"> <!-- WPF syntax: <Setter Property="Content" Value="{Binding}"/> --> <Setter Property="local:SetterValueBindingHelper.PropertyBinding"> <Setter.Value> <local:SetterValueBindingHelper Property="Content" Binding="{Binding}"/> </Setter.Value> </Setter> </Style>
There's really nothing new here, but I did want to show off that SetterValueBindingHelper works for non-ItemsControls as well.
Finally, here's the XAML for the right Button where two properties are being set by SetterValueBindingHelper:
<Style TargetType="Button"> <!-- WPF syntax: <Setter Property="Grid.Column" Value="{Binding}"/> <Setter Property="Grid.Row" Value="{Binding}"/> --> <Setter Property="local:SetterValueBindingHelper.PropertyBinding"> <Setter.Value> <local:SetterValueBindingHelper Type="System.Windows.Controls.Grid, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e" Property="Column" Binding="{Binding}"/> </Setter.Value> </Setter> <Setter Property="local:SetterValueBindingHelper.PropertyBinding"> <Setter.Value> <local:SetterValueBindingHelper Type="Grid" Property="Row" Binding="{Binding}"/> </Setter.Value> </Setter> </Style>
As you can see, SetterValueBindingHelper also supports setting attached DependencyPropertys, so if you find yourself in a situation where you need to style such a creature, SetterValueBindingHelper's got your back. It's also worth pointing out that the Setter for "Column" is using the official assembly-qualified naming for the Type parameter of the SetterValueBindingHelper object. This form is completely unambiguous - and it's also big, ugly, and pretty much impossible to type from memory... :) So I added code to SetterValueBindingHelper that allows users to also specify the full name of a type (ex: System.Windows.Controls.Grid) or just its short name (ex: Grid, used by the Setter for "Row"). I expect pretty much everyone will use the short name - but sleep soundly knowing you can fall back on the other forms "just in case".
System.Windows.Controls.Grid
Grid
[Click here to download the complete source code for SetterValueBindingHelper and the sample application.]
Lastly, here's the code for SetterValueBindingHelper in case that's all you care about:
Update (2010-10-31): At the suggestion of a reader, I've removed the implementation of SetterValueBindingHelper from this post because a newer version of the code (that works well on Silverlight 4, too) can be found in this more recent post. (FYI: The download link is the same for both posts and therefore always up-to-date.)
/// <summary> /// Class that implements a workaround for a Silverlight XAML parser /// limitation that prevents the following syntax from working: /// <Setter Property="IsSelected" Value="{Binding IsSelected}"/> /// </summary> public class SetterValueBindingHelper { /// <summary> /// Optional type parameter used to specify the type of an attached /// DependencyProperty as an assembly-qualified name, full name, or /// short name. /// </summary> [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Unambiguous in XAML.")] public string Type { get; set; } /// <summary> /// Property name for the normal/attached DependencyProperty on which /// to set the Binding. /// </summary> public string Property { get; set; } /// <summary> /// Binding to set on the specified property. /// </summary> public Binding Binding { get; set; } /// <summary> /// Gets the value of the PropertyBinding attached DependencyProperty. /// </summary> /// <param name="element">Element for which to get the property.</param> /// <returns>Value of PropertyBinding attached DependencyProperty.</returns> [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "SetBinding is only available on FrameworkElement.")] public static SetterValueBindingHelper GetPropertyBinding(FrameworkElement element) { if (null == element) { throw new ArgumentNullException("element"); } return (SetterValueBindingHelper)element.GetValue(PropertyBindingProperty); } /// <summary> /// Sets the value of the PropertyBinding attached DependencyProperty. /// </summary> /// <param name="element">Element on which to set the property.</param> /// <param name="value">Value forPropertyBinding attached DependencyProperty.</param> [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "SetBinding is only available on FrameworkElement.")] public static void SetPropertyBinding(FrameworkElement element, SetterValueBindingHelper value) { if (null == element) { throw new ArgumentNullException("element"); } element.SetValue(PropertyBindingProperty, value); } /// <summary> /// PropertyBinding attached DependencyProperty. /// </summary> public static readonly DependencyProperty PropertyBindingProperty = DependencyProperty.RegisterAttached( "PropertyBinding", typeof(SetterValueBindingHelper), typeof(SetterValueBindingHelper), new PropertyMetadata(null, OnPropertyBindingPropertyChanged)); /// <summary> /// Change handler for the PropertyBinding attached DependencyProperty. /// </summary> /// <param name="d">Object on which the property was changed.</param> /// <param name="e">Property change arguments.</param> private static void OnPropertyBindingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // Get/validate parameters var element = (FrameworkElement)d; var item = (SetterValueBindingHelper)(e.NewValue); if ((null == item.Property) || (null == item.Binding)) { throw new ArgumentException( "SetterValueBindingHelper's Property and Binding must both be set to non-null values."); } // Get the type on which to set the Binding Type type = null; if (null == item.Type) { // No type specified; setting for the specified element type = element.GetType(); } else { // Try to get the type from the type system type = System.Type.GetType(item.Type); if (null == type) { // Search for the type in the list of assemblies foreach (var assembly in AssembliesToSearch) { // Match on short or full name type = assembly.GetTypes() .Where(t => (t.FullName == item.Type) || (t.Name == item.Type)) .FirstOrDefault(); if (null != type) { // Found; done searching break; } } if (null == type) { // Unable to find the requested type anywhere throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Unable to access type \"{0}\". Try using an assembly qualified type name.", item.Type)); } } } // Get the DependencyProperty for which to set the Binding DependencyProperty property = null; var field = type.GetField(item.Property + "Property", BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Static); if (null != field) { property = field.GetValue(null) as DependencyProperty; } if (null == property) { // Unable to find the requested property throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "Unable to access DependencyProperty \"{0}\" on type \"{1}\".", item.Property, type.Name)); } // Set the specified Binding on the specified property element.SetBinding(property, item.Binding); } /// <summary> /// Returns a stream of assemblies to search for the provided type name. /// </summary> private static IEnumerable<Assembly> AssembliesToSearch { get { // Start with the System.Windows assembly (home of all core controls) yield return typeof(Control).Assembly; // Fall back by trying each of the assemblies in the Deployment's Parts list foreach (var part in Deployment.Current.Parts) { var streamResourceInfo = Application.GetResourceStream( new Uri(part.Source, UriKind.Relative)); using (var stream = streamResourceInfo.Stream) { yield return part.Load(stream); } } } } }
My teammates and I spent some time last week on an exercise known as "app building" to help identify issues with the latest build of Silverlight, the SDK, and the Silverlight Toolkit. The way app building works is that everyone comes up with an idea for a medium-sized application they think could be built with the bits at hand - then goes off and tries to build as much of that application as they can before time runs out!
The emphasis is on testing new scenarios, coming up with creative ways of integrating components, and basically just getting the same kind of experience with the framework that customers have every day. Coming up with a beautifully architected solution is nice if it happens, but not specifically a goal here. Rather, the point is to help people take a holistic look at how everything works together - because sometimes you'll find that two things which both look good in isolation are quite difficult to use together. App building is a great technique to use as part of the quality assurance process and the time we spent was definitely worthwhile. :)
For my application, I decided to write an organizational hierarchy viewer based loosely on an internal tool managers use at Microsoft. The application offers three main ways to visualize the data: a hierarchical tree of all employees at the left, a flattened summary pane of all employees at the bottom, and a detailed view of the selected employee at the right. I also added a search feature and a simple chart for visualizing the size of someone's "empire". (Because I love me some Charting...) I called my app "HeadTraxExtreme" (partly an inside joke) and here's what it looked like after the two days I spent banging it out:
[If you have the Silverlight 3 Beta installed, click here (or on the image above) to run HeadTraxExtreme in your browser.]
[If you want to have a look at the complete source code or build HeadTraxExtreme yourself, click here to download it.]
Notes:
HeadTraxExtreme.Web
Building HeadTraxExtreme was a fun little diversion that turned up some good issues for everyone. It exposed me to a couple of controls I hadn't used yet and I'm glad to have broadened my knowledge. I think there's probably a little something for everyone here; I hope HeadTraxExtreme can be a good learning experience for others, too!
Let's imagine that we want to use Silverlight (or WPF!) to chart the performance of a book on one of those "bestsellers" lists... The book we care about has been doing very well lately; here's the corresponding data we want to display:
var items = new List<DataItem> { new DataItem(new DateTime(2009, 4, 1), 10), new DataItem(new DateTime(2009, 4, 8), 5), new DataItem(new DateTime(2009, 4, 15), 2), new DataItem(new DateTime(2009, 4, 22), 1), new DataItem(new DateTime(2009, 4, 29), 1), };
Naturally, we'll use the Charting controls that are part of the Silverlight Toolkit (and also available for WPF). :) Charting is easy to use and we quickly bang out the following XAML to create something suitable:
<charting:Chart FontSize="9"> <charting:LineSeries ItemsSource="{Binding}" DependentValuePath="Place" IndependentValuePath="Date" Title="Book"> <charting:LineSeries.DataPointStyle> <Style TargetType="charting:LineDataPoint"> <Setter Property="Background" Value="Maroon"/> </Style> </charting:LineSeries.DataPointStyle> <charting:LineSeries.DependentRangeAxis> <charting:LinearAxis Orientation="Y" Minimum="0.5" Maximum="10.5" Interval="1" ShowGridLines="True"/> </charting:LineSeries.DependentRangeAxis> </charting:LineSeries> </charting:Chart>
It looks like this:
Hurm...
The chart is 100% correct, but there's a problem: it looks like the book is becoming less popular, not more popular. Most of us are used to assuming that "bigger/taller is better", but that's not the case for the data in this scenario and so the chart's meaning is not intuitively obvious. In the ideal world, there would be a bool Invert property on LinearAxis that you could toggle to "flip" the vertical axis and save the day. Unfortunately, we haven't yet implemented such a property (though it's on our list of things to do)...
bool
Invert
LinearAxis
Therefore, it looks like a clever solution is called for - and in this case the simple trick is to invert the values before charting them. After inversion, the "best" values (low numbers like 1) will be numerically greater than the "worst" values (high numbers like 10) and will therefore appear towards the top of the chart. This seems almost too easy, so let's see how it works out in practice by writing a simple IValueConverter to invert the values and then making the highlighted changes to the XAML:
public class InverterConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is int) { return -(int)value; } throw new NotImplementedException(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
<charting:Chart FontSize="9"> <charting:LineSeries ItemsSource="{Binding}" DependentValueBinding="{Binding Place, Converter={StaticResource InverterConverter}}" IndependentValuePath="Date" Title="Book"> <charting:LineSeries.DataPointStyle> <Style TargetType="charting:LineDataPoint"> <Setter Property="Background" Value="Maroon"/> </Style> </charting:LineSeries.DataPointStyle> <charting:LineSeries.DependentRangeAxis> <charting:LinearAxis Orientation="Y" Minimum="-10.5" Maximum="-0.5" Interval="1" ShowGridLines="True"/> </charting:LineSeries.DependentRangeAxis> </charting:LineSeries> </charting:Chart>
The resulting chart:
Woot - the chart now clearly communicates the book's recent popularity! But the trick we played with negative numbers is plainly visible for everyone to see and they will probably mock us mercilessly. :( If only there were some way to customize the chart's visuals to hide what we've done and complete the illusion...
Wait - there is a way! All we need to do is take advantage of Charting's DataPoint.DependentValueStringFormat and LinearAxis.AxisLabelStyle properties and mix in a little of .NET's support for "Section Separators and Conditional Formatting".
DataPoint.DependentValueStringFormat
LinearAxis.AxisLabelStyle
[Type, type, type...]
<charting:Chart FontSize="9"> <charting:LineSeries ItemsSource="{Binding}" DependentValueBinding="{Binding Place, Converter={StaticResource InverterConverter}}" IndependentValuePath="Date" Title="Book"> <charting:LineSeries.DataPointStyle> <Style TargetType="charting:LineDataPoint"> <Setter Property="Background" Value="Maroon"/> <Setter Property="DependentValueStringFormat" Value="{}{0:0.#;0.#}"/> </Style> </charting:LineSeries.DataPointStyle> <charting:LineSeries.DependentRangeAxis> <charting:LinearAxis Orientation="Y" Minimum="-10.5" Maximum="-0.5" Interval="1" ShowGridLines="True"> <charting:LinearAxis.AxisLabelStyle> <Style TargetType="charting:AxisLabel"> <Setter Property="StringFormat" Value="{}{0:0.#;0.#}"/> </Style> </charting:LinearAxis.AxisLabelStyle> </charting:LinearAxis> </charting:LineSeries.DependentRangeAxis> </charting:LineSeries> </charting:Chart>
Presto:
Success - our chart looks exactly how we want it to and we barely even broke a sweat! You can go ahead and pat yourself on the back a few times - then stop spending time imagining Charting scenarios and get back to work! :)
[Click here to download the complete source code for the sample application used to create the charts shown above.]
As I've said before, one of our key goals for the March 09 release of Silverlight/WPF Charting was to improve the performance of key customer scenarios. I didn't go into a lot of details with the release notes, but one of the ways we accomplished this was to change some code that had been doing a linear search to use a binary search instead. (Example: Finding the high/low data point values as part of the process of setting the range of an axis.) If this optimization seems obvious and makes you think "golly, they should have done it that way in the first place", you're absolutely right. :) It's not that we didn't want to do this earlier; it was just that we didn't have the resources to do so...
What we hoped to take advantage of was some already-written-and-tested class implementing an ordered multi-dictionary (a kind of associative array) that could be dropped right into the Silverlight Toolkit source code and used without concern. After we found a suitable implementation, we established that it did, indeed, improve performance on non-trivial data sets in the way that we hoped. Unfortunately, something came up at the last minute and we decided not to proceed with the code we'd been using for the previous few weeks. That left us in kind of a funk because we didn't want to give up the performance improvements we'd already seen...
So I set aside some other tasks and dashed off a quick binary tree implementation to do what we needed and preserve the performance gains from faster searching. The resulting code for BinaryTree is part of the Charting source code and can be viewed here or as part of the Silverlight Toolkit download. It's fairly simple and straightforward, though there are a few things worth calling out:
BinaryTree
KeyAndValueComparison
Traverse
GetKeys
GetValuesForKey
GetValuesForAllKeys
GetExtreme
MinimumKey
MaximumKey
MinimumValue
MaximumValue
So that's a bit of background on the BinaryTree class that's used by Silverlight/WPF Charting today. An unbalanced binary tree doesn't give the best performance in the world, but it was quick and easy to implement (under time pressure!) and gives a noticeable boost to many of the common scenarios we set out to improve.
And as it happens, this is all very relevant to the topic of my next post! Here's a hint; see if you can guess what it is... :)