The blog of dlaa.me

Posts tagged "WPF"

Silverlight (and WPF) Data Visualization classes unsealed [Silverlight Toolkit October 2009 release now available!]

We've just published the October 2009 release of the Silverlight Toolkit as part of today's .NET 4 and Visual Studio 2010's Beta 2 release! One of the big things we've done with this release of the Toolkit is to add rich support for Visual Studio 2010's vastly improved Silverlight design-time experience. In fact, the new VS 2010 design-time experience has gotten so good that some developers have stopped using Blend altogether! :) I encourage everyone to have a look at the live samples for the latest release of the Silverlight 3 Toolkit, download the Toolkit installer, and try for yourself!

Other big news for this release is the introduction of comprehensive, WPF-compatible drag-and-drop support for Silverlight! Although this support doesn't extend outside the web browser (that would require changes to Silverlight itself), it enables full-fidelity drag-and-drop experiences within the browser using the same API that WPF users are already accustomed to. And if that wasn't enough, there are also a collection of drag-and-drop-friendly "wrapper controls" for common scenarios (ex: ListBox, TreeView, and DataGrid) that make it trivial to add support for drag-and-drop to an existing control. Dragging and dropping within a control (to re-order items) or between controls (to move items around) is now just a few lines of XAML away! (Note: No code changes necessary!) But wait, there's more: There's also a wrapper for Charting's DataPointSeries that enables drag-and-drop into and out of a live Chart control! This really needs to be seen to be believed, so please visit the "Drag and Drop" page of the public samples for a great example of this. Then go read Jafar's post about the new drag/drop support for all the juicy details!

Note: The October 09 release of the Silverlight Toolkit includes binaries for Silverlight 3 only. Now that Silverlight 3 has been out for a few months and is fully backward-compatible with all Silverlight 2 applications, we expect that everyone has upgraded from Silverlight 2 and are therefore no longer actively developing the Toolkit for Silverlight 2. Of course, if some of you have a specific need for Silverlight 2 Toolkit bits, previous releases continue to be available to download from CodePlex!

 

With the introductory stuff out of the way, let's move on to the details of changes to the Data Visualization assembly and the corresponding improvements to Silverlight and WPF Charting. My recent post on Data Visualization Development Release 1 has already discussed most of these changes at length, so I'm just going to include the change descriptions here. For more detail on the motivation behind these changes or their implications for current and future possibilities, please refer back to that post.

Notable Changes

Unsealed (i.e., removed the "sealed" modifier from) all core Data Visualization classes. Although we aren't yet completely settled on the public-facing API for Data Visualization and reserve the right to make breaking changes in the future, these classes are being unsealed now to help simplify a wide variety of user scenarios that are being actively developed and that are cumbersome without the ability to subclass (without needing to create a private build of the assembly solely for the purpose of unsealing these classes). Other changes were kept to a minimum, but a couple of methods have been changed to protected virtual for consistency and/or convenience as well as some tweaks that resulted due to new code analysis warnings due to explicit interface implementations in an unsealed class.

Introduced ISeries interface to Charting as "base interface" for all Series. This allows users to write ItemsControl-based Series which will automatically leverage all of the ItemsControl infrastructure for creating points, tracking data changes, etc. and also gives us a safe root for a future 3D series hierarchy. As part of this change, some interfaces have been cleaned up a bit (IStyleDispenser, ISeriesHost) and others have been created (IStyleDispenser.StylesChanged event). Also, some public methods with little justification have been removed/made private/moved lower (Chart.Refresh, Chart.ResetStyles, StyleDispenser.ResetStyles) and some vestigial code has been removed (ISeriesHost.GlobalSeriesIndexesInvalidated).

Various usability improvements. Updated Series to look for "LegendItemStyle" in their ResourceDictionary for increased customizability. Added Owner property to LegendItem pointing to owning Series instance to simplify LegendItem-based user scenarios. Added ActualDataPointStyle and ActualLegendItemStyle properties and used Bindings to automatically propagate changes to the right places. (Aside: This fixes a bug that was reported against the WPF Toolkit as I was making the change!) Moved code so that PieSeries now has the DataPointStyle property like the other Series. Updated LegendItem default Template to include standard TemplateBindings for Background/BorderBrush/BorderThickness for more friendly designer experience.

Breaking Changes

Renamed Charting's StylePalette to Palette (for clarity) AND changed its type to IEnumerable<ResourceDictionary> (from IEnumerable<Style>) for a significant flexibility boost. Performed related renamings (many internal/private): IStyleDispenser->IResourceDictionaryDispenser, StylePalette->ResourceDictionaryCollection, StyleDispensedEventArgs->ResourceDictionaryDispensedEventArgs, StyleDispenser->ResourceDictionaryDispenser, StyleEnumerator->ResourceDictionaryEnumerator.

Most notably, this change makes it possible to associate MULTIPLE things with a palette entry and enables designers to easily and flexibly customize things like the LineSeries PolyLineStyle in the Palette. Additionally it enables the use of DynamicResource (currently only supported by the WPF platform) to let users customize their DataPointStyle without inadvertently losing the default/custom Palette colors. (Note: A very popular request!) Thanks to merged ResourceDictionaries, this also enables the addition of arbitrary resources at the Palette level (like Brushes) which can then be referenced by DataPoints, etc..

Changed return value of Charting's IAxis.GetPlotAreaCoordinate from UnitValue? to UnitValue to better support custom Axis implementations. Specifically, some numeric axis types (logarithmic axis, for example) don't support all numeric values and need a way to indicate that certain values (ex: <= 0 for logarithmic) are "not supported" for plotting. This was previously done by returning a null value, but now the code should return a UnitValue with Value=double.NaN. Convenience method UnitValue.NaN has been added to create such values easily. Because the Series implementations already need to handle NaN values, this change collapses two different edge cases into one and simplifies the code accordingly. Added code to Series to handle this situation by hiding (via Visibility=Collapsed) DataPoints on coordinates that are not valid.

One notable consequence of this change is that the Visibility of DataPoints is now controlled by the Series and will be set to Visible or Collapsed as necessary. Therefore, any customizations that directly set this property may no longer work, but there are other simple ways of achieving the same effect and this change is not expected to cause any difficulty. For example, the "Sparkline" demo of the samples project was affected by this change because it provided a custom DataPointStyle that set Visibility to Collapsed. The fix is not only trivial, but an improvement: change the Style to specify a null Template instead!

Other Changes

Remove unnecessary code. Moved duplicated DependencyProperties IRangeAxis DependentRangeAxis and IAxis IndependentAxis from ColumnSeries and BarSeries into common base class ColumnBarBaseSeries. Moved duplicated DependencyProperties IRangeAxis DependentRangeAxis and IAxis IndependentAxis from AreaSeries and LineSeries into common base class LineAreaBaseSeries. Made similar changes for methods OnApplyTemplate and UpdateDataPoint and half of UpdateShape.

Simplified default Palette Brushes by removing ScaleTransform and TranslateTransform and replacing with RadialBrush. The on-screen visuals remain the same, but the XAML is considerably smaller and simpler - and should be a bit quicker to render as well!

Various other small changes.

 

Of the two breaking changes, only the rename to Palette is likely to affect most people. Fortunately, converting existing code/XAML is really quite simple - which you can see as I recycle the example I gave previously.

The old way:

<chartingToolkit:Chart Title="Statistics (Custom Palette)">
    <chartingToolkit:Chart.StylePalette>
        <visualizationToolkit:StylePalette>
            <Style TargetType="Control">
                <Setter Property="Background" Value="Blue"/>
            </Style>
            <Style TargetType="Control">
                <Setter Property="Background" Value="Green"/>
            </Style>
            <Style TargetType="Control">
                <Setter Property="Background" Value="Red"/>
            </Style>
        </visualizationToolkit:StylePalette>
    </chartingToolkit:Chart.StylePalette>
    ...
</chartingToolkit:Chart>

And the new way (with changes highlighted):

<chartingToolkit:Chart Title="Statistics (Custom Palette)">
    <chartingToolkit:Chart.Palette>
        <visualizationToolkit:ResourceDictionaryCollection>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Blue"/>
                </Style>
            </ResourceDictionary>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Green"/>
                </Style>
            </ResourceDictionary>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Red"/>
                </Style>
            </ResourceDictionary>
        </visualizationToolkit:ResourceDictionaryCollection>
    </chartingToolkit:Chart.Palette>
    ...
</chartingToolkit:Chart>

It's pretty clear that once you've done this once, it'll be easy to do anywhere else your project requires. I explained the motivations for this change previously, so I won't repeat myself here - I just wanted to call out how straightforward the upgrade is expected to be.

 

Clearly, the big news for Data Visualization is the unsealing of the primary charting classes! Because I went into great detail on this earlier, I won't spend a lot of time on that here. Instead, I'd like to call out a particularly timely and relevant use of the new subclassing ability: Cory Plotts' LogarithmicAxis implementation for WPF and Silverlight! What's great about what he's done is that logarithmic axis support is one of our most requested features, and something we haven't had a chance to implement yet. I've always hoped that somebody in the community would be able to step up and share something here, so I was really excited to see Cory's blog post. If you're one of the users who's been waiting for a logarithmic axis, please have a look at Cory's implementation and see if it does what you need!

Aside: You might be wondering why we haven't gotten to logarithmic axis ourselves... Well, as you may be aware, we've been operating under severe resource constraints from the beginning, and that forces us to try to choose our investments carefully. When we're trying to decide between two features and one of them constitutes a change to the core of the Charting framework while the other is something that derives from an existing class to build on top of the framework, we'll tend to make the core framework change and hope that the community is able to help with the subclassing change. Honestly, this seems like the right balance to me and is a large part of why we're unsealing now even though the Charting APIs aren't completely set in stone.

Along similar lines, I encourage people who have been wanting to annotate their PieSeries charts to have a look at the fantastic work Bea Stollnitz has done: Part 1, Part 2, Part 3. Bea built on top of the sealed Charting hierarchy using some pretty clever tricks and techniques. But now that we've unsealed, it's my hope that she'll be able to take advantage of that to spend more time working on the great features she's adding and less time trying to jump through artificial hoops. :)

 

There are two other things I'd like to call out here:

As always, if you have any questions or feedback, the right places to start are the Silverlight Discussion Forum or the WPF Discussion List. Bugs and feature requests can be logged with the Silverlight Issue Tracker or the WPF Issue Tracker. Please raise issues that are clearly unique to one platform or the other in the obvious place. But for general questions and things that are common to both platforms, the Silverlight forum/list is probably a better place because there's more context and history there.

 

Thanks very much for your interest in Silverlight and WPF Data Visualization - I hope you like the improvements!

Give your computer insomnia [Free tool and source code to temporarily prevent a machine from going to sleep!]

The default power settings for Windows are set up so a computer will go to sleep after 15 to 30 minutes of inactivity (i.e., no mouse or keyboard input). This is great because a computer that's not being used doesn't need to be running at full power. By letting an idle machine enter sleep mode, the user benefits from a significant reduction in electricity use, heat generation, component wear, etc.. And because sleep mode preserves the state of everything in memory, it's quick to enter, quick to exit, and doesn't affect the user's work-flow. All the same applications continue running, windows stay open and where they were, etc.. So sleep mode is a Good Thing and I'm a fan.

However, sometimes a computer is busy even though someone isn't actively using the mouse and keyboard; common examples include playing a movie, burning a DVD, streaming music, etc.. In these cases, you don't want the machine to go to sleep because you're using it - even though you're not actually using it! So most media players and disc burners tell Windows not to go to sleep while they're running. In fact, there's a dedicated API for exactly this purpose: the SetThreadExecutionState Win32 Function.

But what about those times when the computer is busy doing something and the relevant program doesn't suppress the default sleep behavior? For example, it might be downloading a large file, re-encoding a music collection, backing up the hard drive, or hashing the entire contents of the disk. You don't want the machine to go to sleep for now, but are otherwise happy with the default sleep behavior. Unfortunately, the easiest way I know of to temporarily suppress sleeping is to go to Control Panel, open the Power Options page, change the power plan settings, commit them - and then remember to undo everything once the task is finished. It's not hard; but it's kind of annoying...

 

So here's a better way:

Insomnia application

Insomnia is a simple WPF application that calls the SetThreadExecutionState API to disable sleep mode for as long as it's running. (Note that the display can still power off during this time - it's just sleep for the computer that's blocked.) Closing the Insomnia window immediately restores whatever sleep mode was in effect before it was run. It couldn't be easier!

 

[Click here to download the Insomnia application along with its complete source code.]

 

Notes:

  • Insomnia basically boils down to a single function call - but to a function that's a Win32 API and is not part of the .NET Framework. This is where a very powerful feature saves the day: Platform Invoke. For those who aren't familiar with it, P/Invoke (as it's called) lets a managed application call into the APIs exposed by a native DLL. All it takes is a dash of the DllImport attribute and a little bit of translation from the Win32 types to their managed counterparts. The MSDN documentation goes into lots of detail here, and I encourage interested parties to go there.
  • One popular resource for P/Invoke assistance is PInvoke.net, where you can find managed definitions for hundreds of native APIs. But I usually just end up creating my own - if nothing else, it's a good learning exercise. :)
  • The Insomnia window has its Topmost property set to True so it's always visible and people will be less likely to accidentally leave their computers sleep-less. Other than taking up a small bit of screen space and memory, Insomnia consumes no system resources, so it won't get in the way of whatever else is running.
  • It is considered polite to leave things the way you found them, so Insomnia makes an extra call to SetThreadExecutionState when it's closed in order to restore things to how they were. However, this is really more for show than anything else, because the execution state is clearly per-thread and Insomnia's thread is about to die anyway.
  • I did a quick web search for similar tools before I wrote Insomnia and there are a few out there. Most of what I found was for Linux and Mac for some reason, but I'm sure Insomnia isn't the first of its kind for Windows. However, that doesn't stop it from being a nice introduction to P/Invoke - and besides, I'm always happier running my own code! :)

 

Finally, here's the implementation:

public partial class Window1 : Window
{
    private uint m_previousExecutionState;

    public Window1()
    {
        InitializeComponent();

        // Set new state to prevent system sleep (note: still allows screen saver)
        m_previousExecutionState = NativeMethods.SetThreadExecutionState(
            NativeMethods.ES_CONTINUOUS | NativeMethods.ES_SYSTEM_REQUIRED);
        if (0 == m_previousExecutionState)
        {
            MessageBox.Show("Call to SetThreadExecutionState failed unexpectedly.",
                Title, MessageBoxButton.OK, MessageBoxImage.Error);
            // No way to recover; fail gracefully
            Close();
        }
    }

    protected override void OnClosed(System.EventArgs e)
    {
        base.OnClosed(e);

        // Restore previous state
        if (0 == NativeMethods.SetThreadExecutionState(m_previousExecutionState))
        {
            // No way to recover; already exiting
        }
    }

    private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
    {
        // Start an instance of the NavigateUri (in a browser window)
        Process.Start(((Hyperlink)sender).NavigateUri.ToString());
    }
}

internal static class NativeMethods
{
    // Import SetThreadExecutionState Win32 API and necessary flags
    [DllImport("kernel32.dll")]
    public static extern uint SetThreadExecutionState(uint esFlags);
    public const uint ES_CONTINUOUS = 0x80000000;
    public const uint ES_SYSTEM_REQUIRED = 0x00000001;
}

If it walks like a duck and talks like a duck, it must be a ... TreeGrid! [A simple, XAML-only TreeGrid UI for WPF]

If you've done much work with WPF or Silverlight, chances are you already know what a TreeView is and what a DataGrid is. You know that a TreeView is good for showing hierarchical data and a DataGrid is good for showing tabular data. But you may not know about their hybrid love child, the TreeGrid - and that's what this post is about.

Sometimes you've got data that's basically tabular in nature, yet also has a hierarchical aspect, and you'd like to leverage that to give people control over the level of detail they're seeing. Most commonly, you'll see a TreeGrid used when the tabular data can be nicely summarized (or "rolled up") into hierarchical groupings. For example, a list of people's name and address would not make a good TreeGrid, because there's no natural grouping that makes sense (you can't combine addresses). However, a list of people's company and salary might make a good TreeGrid because it's natural to group by job and the aggregated salary information could be informative (either as an average or as a sum).

Aside: You might wonder if DataGrid's native support for grouping would be useful here. In my experience, DataGrids don't tend to summarize the grouped data like we want - but if you have examples to the contrary, I'd love to see them.

So with all this talk about TreeGrid, you might expect to find one in the Silverlight or WPF framework, or perhaps as part of the Silverlight Toolkit or WPF Toolkit. But you won't - it's just not used frequently enough to have made it to the big leagues yet. The good news is that a bit of web searching will turn up some third-party TreeGrid options that definitely seem worth evaluating. But because I'm cheap and a show-off - and occasionally fall victim to a little NIH - I decided to craft a TreeGrid-like experience using only the WPF TreeView control, a couple of Grids, and some XAML.

That's right - no code, just XAML! :)

 

Here's how my SimpleTreeGridUX sample looks with some data I made up about the schedule of a fictional developer:

SimpleTreeGridUX sample

And here's the complete XAML:

<!-- TreeGrid "Control" -->
<Border BorderBrush="Black" BorderThickness="1">

    <!-- Resources -->
    <Border.Resources>
        <Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}">
            <Setter Property="Margin" Value="3 0 3 0"/>
        </Style>
        <Style x:Key="TextBlockBoldStyle" TargetType="{x:Type TextBlock}" BasedOn="{StaticResource TextBlockStyle}">
            <Setter Property="FontWeight" Value="Bold"/>
        </Style>
    </Border.Resources>

    <!-- Content -->
    <Grid Grid.IsSharedSizeScope="True">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <!-- Column headers -->
        <TreeViewItem Grid.Row="0" BorderThickness="1">
            <TreeViewItem.Header>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition SharedSizeGroup="Task"/>
                        <!-- Placeholders for two columns of ToggleButton -->
                        <ColumnDefinition SharedSizeGroup="Toggle"/>
                        <ColumnDefinition SharedSizeGroup="Toggle"/>
                        <ColumnDefinition SharedSizeGroup="Duration"/>
                        <ColumnDefinition SharedSizeGroup="Notes"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Column="0" Text="Task" Style="{StaticResource TextBlockBoldStyle}"/>
                    <!-- Empty TreeViewItem to measure the size of its ToggleButton into the "Toggle" group-->
                    <TreeViewItem Grid.Column="1" Padding="0"/>
                    <TextBlock Grid.Column="3" Text="Duration" Style="{StaticResource TextBlockBoldStyle}"/>
                    <TextBlock Grid.Column="4" Text="Notes" Style="{StaticResource TextBlockBoldStyle}"/>
                </Grid>
            </TreeViewItem.Header>
        </TreeViewItem>

        <!-- Data rows -->
        <TreeView Grid.Row="1" ItemsSource="{Binding SubItems}" BorderBrush="Gray" BorderThickness="0 1 0 0">
            <TreeView.ItemTemplate>

                <!-- Level 0 template leaves space for 2 child "Toggle" levels -->
                <HierarchicalDataTemplate ItemsSource="{Binding SubItems}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition SharedSizeGroup="Task"/>
                            <ColumnDefinition SharedSizeGroup="Toggle"/>
                            <ColumnDefinition SharedSizeGroup="Toggle"/>
                            <ColumnDefinition SharedSizeGroup="Duration"/>
                            <ColumnDefinition SharedSizeGroup="Notes"/>
                        </Grid.ColumnDefinitions>
                        <TextBlock Grid.Column="0" Text="{Binding Task}" Style="{StaticResource TextBlockStyle}"/>
                        <TextBlock Grid.Column="3" Text="{Binding Duration}" Style="{StaticResource TextBlockStyle}"/>
                        <TextBlock Grid.Column="4" Text="{Binding Notes}" Style="{StaticResource TextBlockStyle}"/>
                    </Grid>

                    <!-- Level 1 template leaves space for 1 child "Toggle" level -->
                    <HierarchicalDataTemplate.ItemTemplate>
                        <HierarchicalDataTemplate ItemsSource="{Binding SubItems}">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition SharedSizeGroup="Task"/>
                                    <ColumnDefinition/>
                                    <ColumnDefinition SharedSizeGroup="Toggle"/>
                                    <ColumnDefinition SharedSizeGroup="Duration"/>
                                    <ColumnDefinition SharedSizeGroup="Notes"/>
                                </Grid.ColumnDefinitions>
                                <TextBlock Grid.Column="0" Text="{Binding Task}" Style="{StaticResource TextBlockStyle}"/>
                                <TextBlock Grid.Column="3" Text="{Binding Duration}" Style="{StaticResource TextBlockStyle}"/>
                                <TextBlock Grid.Column="4" Text="{Binding Notes}" Style="{StaticResource TextBlockStyle}"/>
                            </Grid>

                            <!-- Level 2 template has no children -->
                            <HierarchicalDataTemplate.ItemTemplate>
                                <HierarchicalDataTemplate ItemsSource="{Binding SubItems}">
                                    <Grid>
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition SharedSizeGroup="Task"/>
                                            <ColumnDefinition/>
                                            <ColumnDefinition/>
                                            <ColumnDefinition SharedSizeGroup="Duration"/>
                                            <ColumnDefinition SharedSizeGroup="Notes"/>
                                        </Grid.ColumnDefinitions>
                                        <TextBlock Grid.Column="0" Text="{Binding Task}" Style="{StaticResource TextBlockStyle}"/>
                                        <TextBlock Grid.Column="3" Text="{Binding Duration}" Style="{StaticResource TextBlockStyle}"/>
                                        <TextBlock Grid.Column="4" Text="{Binding Notes}" Style="{StaticResource TextBlockStyle}"/>
                                    </Grid>
                                </HierarchicalDataTemplate>
                            </HierarchicalDataTemplate.ItemTemplate>
                        </HierarchicalDataTemplate>
                    </HierarchicalDataTemplate.ItemTemplate>
                </HierarchicalDataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Border>

 

Notes:

  • There are two "tricks" I use to get DataGrid-like behavior from a TreeView. The first is the Grid.IsSharedSizeScope attached DependencyProperty and its partner-in-crime DefinitionBase.SharedSizeGroup. (Both available only on WPF for now.) By setting IsSharedSizeScope on a parent element and SharedSizeGroup on some of the column/row definitions of Grids within it, it's possible to "link" the sizes of cells across different Grids. In the scenario above, that sharing takes place across the separate Grids of the column headers and each TreeViewItem row of data. In this manner, same-width columns are created for the "Task", "Duration", and "Notes" fields so they all line up properly. Except that they wouldn't actually line up if it weren't for...
  • The "Toggle" shared size group which is used to offset TreeViewItem children to take into account the indent that TreeViewItem parents automatically impose on them. The following diagram of the default TreeViewItem layout should help explain what I mean:
    Header
    Children
    You see, each collection of children is offset to the right by exactly the width of the TreeViewItem's toggle element. So if all we did was make each column's cells the same width, things wouldn't actually line up because of this offset showing up in different amounts everywhere:
    Header
    Header
    Children
    So what I've done is add a special shared size group with exactly the same width as the toggle (this is what the empty TreeViewItem in the header section of the XAML is for). Having done that, the "Toggle" size group can be used to simulate the width of the actual toggle anywhere it's needed. Therefore, I'm able to insert the appropriate counter-offset for each level of the tree - and everything lines up beautifully!
  • You've probably noticed that there's an unfortunate amount of XAML duplication above - each of the three HierarchicalDataTemplates are very nearly identical. In fact, the only difference among them is whether the second and third ColumnDefinition have the SharedSizeGroup property or not. Now, I strive to stay as DRY as the next guy, and I tried to come up with a way to collapse the three templates into one. But while I could do so quite easily using a bit of code, I couldn't come up with a nice way that was pure XAML.
    Aside: Converting the template contents to a UserControl gets close, but there's still the problem of toggling that property based on external input. And while it would definitely be possible to decorate the (view) model with information about each element's level, I considered that to be "cheating" for the purposes of this exercise. :)

 

[Click here to download the complete source code for the SimpleTreeGridUX sample.]

 

Just in case it's not really obvious by now, what I describe here is not a true TreeGrid control! While I'll suggest that it looks like a real TreeGrid, behaves mostly like one, and is probably good enough for many scenarios, I'm also the first to acknowledge it's not a true TreeGrid. A true TreeGrid would probably have an extensive API for managing columns and rows, allow sorting and arbitrary nesting, and end up with an object model pretty similar to DataGrid's. So if you came here looking for a proper TreeGrid control, I'm sorry to disappoint you - but if you came here hoping to learn more about solving real-world problems with WPF, I hope this has been educational! :)

I get by with a little help from my friends [PieSeries annotations trilogy complete!]

Friend and fellow Charting fan Bea Stollnitz has just completed a 3-post series describing how to add annotations to pie charts created by the Data Visualization package that's part of the Silverlight Toolkit and WPF Toolkit. Because annotations are a feature that we'd love to implement ourselves (but haven't had time for yet), I'm delighted that someone in the community has taken this task on - and shared the experience for the benefit of others!

Here are direct links to Bea's posts:

  1. How can I add labels to a WPF pie chart?
  2. How can I add labels to a WPF pie chart? – Implementation details
  3. How can I port the WPF labeled pie chart to Silverlight?

My thanks go out to Bea for sharing her time and expertise here - I hope others find this as cool as I do! :)

 

PS - Please note that while a number of WPF-to-Silverlight incompatibilities are identified in the third post, none of them come from the Data Visualization assembly. We've specifically spent a good bit of effort to make the Silverlight and WPF code/XAML experience identical for Data Visualization; it's the success of projects like this one that are the reason and reward!

PPS - In the next release of the Data Visualization assembly (which you can preview now!), the core Charting classes will no longer be sealed and some of the inconvenience mentioned in the second post should go away.

PPPS - Here's my own take on a quick-and-easy way to add simple annotations to ColumnSeries.

PPPPS - If you're looking for more information about the Silverlight/WPF Data Visualization assembly, I've collected a bunch of links from across the web - including all of my own introductions and notes.

A preview of upcoming Charting changes [Silverlight/WPF Data Visualization Development Release 1]

It was about two months ago that I posted about Silverlight/WPF Data Visualization Development Release 0. At the time, I explained how I was hoping to do occasional, out-of-band releases of the Data Visualization assembly that's part of the Silverlight Toolkit and WPF Toolkit in order to give people an early glimpse of upcoming changes and maybe get a bit of feedback along the way.

It's that time again...

 

Announcing Silverlight/WPF Data Visualization Development Release 1!

 

As usual, there have been plenty of distractions these past weeks to keep us, um..., distracted - but we've still managed to make some significant architectural tweaks that I think people are going to appreciate. Maybe a little less so in the short term because there are a couple of breaking changes, but definitely in the long term because these changes enable some very interesting scenarios and should do a lot to make it easier to develop with the Data Visualization framework.

Please bear in mind that this is just a development release, so it hasn't gone through the same level of scrutiny that our official releases get. Therefore, there may be some behavioral anomalies - and if there are, I apologize in advance. So if you do find an issue, please contact me (by leaving a comment below or by clicking the Email link on my blog) as I'd love to fix whatever I can before the next official release!

Because I'm trying to keep the cost of doing Development Releases down, I'm not doing my usual long-winded write up of new features. Instead, I'm going to include the notable changeset descriptions from our source control system along with a few brief notes. If that isn't enough detail to get you excited, then you're probably not the target audience for these Development Releases. ;)

 

Notable Changes (from the check-in comments)

Unseal (i.e., remove the "sealed" modifier) from all Data Visualization classes that were previously sealed. Although we aren't yet completely settled on the public-facing API for Data Visualization and reserve the right to make breaking changes in the future, these classes are being unsealed now to help simplify a wide variety of user scenarios that are being actively developed and that are cumbersome without the ability to subclass (without needing to create a private build of the assembly solely for the purpose of unsealing these classes). Other changes were kept to a minimum, but a couple of methods have been changed to protected virtual for consistency and/or convenience as well as some tweaks that resulted due to new code analysis warnings due to explicit interface implementations in an unsealed class.

While this is the most controversial change, I think it's the right thing for us to do now. The concern is that people will take our unsealing as encouragement to go off and subclass everything - and then be disappointed/frustrated when they need to change their code after we make some subsequent breaking change to the API. And I sympathize with this concern - so if you're worried about this happening to you, just pretend everything's still sealed for now. The official indication that the API has stabilized will be when the classes in the Data Visualization assembly change quality bands from Preview (their current band) to Stable. That's not happening yet, so please be aware that there's a certain amount of risk when making the decision to build on the current API.

That said, I'm of the opinion that we have little to lose with this because the decision is entirely in the customers' hands. If you don't want the risk, don't take it. But if you're doing something cool with Charting and wish you could subclass to avoid a bunch of additional effort, then this change is for you. :) For instance, Bea Stollnitz is doing some cool stuff with adding annotations to PieSeries - and she's basically called us out in that post for making her task harder because our classes are sealed. Well, discouraging folks from using the Data Visualization assembly is the last thing I'm trying to do - so we're unsealing now to help make the platform as friendly as possible.

And while you're busy taking advantage of the new ability to subclass, I fully expect there will be places we haven't exposed all the extensibility points people want. When that happens, please let me know, and we'll look into addressing that oversight in a future release. Think of it as the "You scratch our backs, we'll scratch yours" model of software development... :)

 

Introduce ISeries interface to Charting as "base interface" for all Series. This will allow users to write ItemsControl-based Series which will automatically leverage all of the ItemsControl infrastructure for creating points, tracking data changes, etc. and also gives us a safe root for a future 3D series hierarchy. As part of this change, some interfaces have been cleaned up a bit (IStyleDispenser, ISeriesHost) and others have been created (IStyleDispenser.StylesChanged event). Also, some public methods with little justification have been removed/made private/moved lower (Chart.Refresh, Chart.ResetStyles, StyleDispenser.ResetStyles) and some vestigial code has been removed (ISeriesHost.GlobalSeriesIndexesInvalidated). Aside from adjusting for renamed/deleted functionality, all tests continue to pass as-is.

There are two pretty big wins from this change - so go back and re-read that paragraph if you weren't paying attention. We've prototyped both an ItemsControl-based PieSeries and a (WPF-only) Viewport3D-based PieSeries and the results are very promising! Simply by changing our base Series contract from a class to an interface, we give people with simple needs the ability to leverage the existing ItemsControl framework and significantly decrease the amount of code they need to understand and interact with. (Aside: There have even been suggestions to change our existing series over to use this model!) And the benefits for 3D are also compelling - though further off in the future due to a variety of open issues and unanswered questions. I'm not going to dwell on the implications of this change more right now, but there are obviously some cool possibilities that I'd love to see folks start to explore. (Hint, hint, friends of Charting...)

 

Rename Charting's StylePalette to Palette (for clarity) AND change its type to IEnumerable<ResourceDictionary> (from IEnumerable<Style>) for a significant flexibility boost. Perform related renamings (many internal/private): IStyleDispenser->IResourceDictionaryDispenser, StylePalette->ResourceDictionaryCollection, StyleDispensedEventArgs->ResourceDictionaryDispensedEventArgs, StyleDispenser->ResourceDictionaryDispenser, StyleEnumerator->ResourceDictionaryEnumerator. Modify all code, comments, tests, samples, themes, etc. accordingly.

Most notably, this change gives us the ability to associate MULTIPLE things with a palette entry and enables designers to easily and flexibly customize things like LineSeries PolyLineStyle in the Palette. Additionally it enables the use of DynamicResource (currently only supported by the WPF platform) to let users customize their DataPointStyle *without* inadvertently losing the default/custom Palette colors. Due to merged ResourceDictionaries, this also enables the addition of arbitrary resources at the Palette level (like Brushes) which can be referenced by DataPoints, etc..

Also: Simplify default Background Brushes by removing ScaleTransform and TranslateTransform and replacing with RadialBrush properties, and more...

This is going to be the most painful change for existing users of Data Visualization - sorry! If you've ever customized a StylePalette, your XAML is going to need to change. However, the opportunities this change opens up seem sufficiently compelling that we've decided to make it now (while we still have the freedom to do so). The good news is that the migration is really quite simple - and once you've seen it done once, you can mindlessly apply the change everywhere it's needed.

To prove it, here's a sample of the "old" way from my original Charting Introduction post:

<chartingToolkit:Chart Title="Statistics (Custom Palette)">
    <chartingToolkit:Chart.StylePalette>
        <visualizationToolkit:StylePalette>
            <Style TargetType="Control">
                <Setter Property="Background" Value="Blue"/>
            </Style>
            <Style TargetType="Control">
                <Setter Property="Background" Value="Green"/>
            </Style>
            <Style TargetType="Control">
                <Setter Property="Background" Value="Red"/>
            </Style>
        </visualizationToolkit:StylePalette>
    </chartingToolkit:Chart.StylePalette>
    ...
</chartingToolkit:Chart>

And here's that same XAML converted to the "new" way of doing things (I've highlighted the changes):

<chartingToolkit:Chart Title="Statistics (Custom Palette)">
    <chartingToolkit:Chart.Palette>
        <visualizationToolkit:ResourceDictionaryCollection>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Blue"/>
                </Style>
            </ResourceDictionary>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Green"/>
                </Style>
            </ResourceDictionary>
            <ResourceDictionary>
                <Style x:Key="DataPointStyle" TargetType="Control">
                    <Setter Property="Background" Value="Red"/>
                </Style>
            </ResourceDictionary>
        </visualizationToolkit:ResourceDictionaryCollection>
    </chartingToolkit:Chart.Palette>
    ...
</chartingToolkit:Chart>

Yes, the new syntax is a little bit longer, no doubt about that - but the ability to associate multiple resources with a single palette entry addresses some very tricky problems we've been avoiding till now. And the DynamicResource benefits for WPF address the single biggest complaint people have had with StylePalette: Why should I have to redefine the entire color palette when all I want to do is provide a new template? This is a really powerful shift, and something I'll probably spend more time showing off in a future blog post.

 

Update Series to look for "LegendItemStyle" in their ResourceDictionary for increased customizability. Add Owner property to LegendItem pointing to owning series to simplify LegendItem-based user scenarios. Add ActualDataPointStyle and ActualLegendItemStyle properties and use Bindings to automatically propagate changes to the right places. (Aside: This fixes a bug that was reported against the WPF Toolkit *as I was making this change*!) Move code so that PieSeries now has the DataPointStyle property like the other Series. Update LegendItem default Template to include standard TemplateBindings for Background/BorderBrush/BorderThickness for more friendly designer experience.

Hey, look, we're already making use of the new ResourceDictionaryCollection! :) The other two big improvements here are the comprehensive use of bindings for the DataPointStyle property that fixes an issue a few customers have bumped into (it's confusing unless you know exactly what's going on) and the addition of DataPointStyle to PieSeries which is like the DynamicResource change in that it should do a lot to simplify things on the WPF platform.

 

Move unnecessarily duplicated DependencyProperties IRangeAxis DependentRangeAxis and IAxis IndependentAxis from ColumnSeries and BarSeries into common base class ColumnBarBaseSeries. Move unnecessarily duplicated DependencyProperties IRangeAxis DependentRangeAxis and IAxis IndependentAxis from AreaSeries and LineSeries into common base class LineAreaBaseSeries. Same for methods OnApplyTemplate and UpdateDataPoint and half of UpdateShape. Also remove an unnecessary override from CategoryAxis. No functional impact.

Less code, same features, no functional impact - 'nuff said.

 

[Please click here to download the complete SilverlightWpfDataVisualization solution (includes all source code and pre-compiled binaries for both platforms).]

 

Release Notes

  • When you add a project reference to the Data Visualization assembly on WPF, you also need to add a reference to WPFToolkit.dll or you'll get weird runtime errors because the Visual State Manager (VSM) isn't available.
  • The file structure of the ZIP archive remains the same (see my previous post for details).
  • Design-time assemblies are not part of the Development Releases because I don't expect the target audience to need them and because they add additional complexity.
  • I haven't updated my DataVisualizationDemos application for this unofficial release.

 

So there you have it: the second Silverlight/WPF Data Visualization Development Release in a nutshell. Though, in many ways, this is really the first release because the previous release didn't showcase upcoming changes like this one does (it mainly set the stage for future releases). I said before that these Development Releases are an experiment - so please let me know if you find them useful or if you'd all rather just wait for an official release and find out what's changed then. I'm hopeful that early access to the new code will be helpful to our early adopters and that we'll be able to incorporate their feedback to deliver an even more compelling, more reliable official release.

So this is me trying to do my part; the ball is in your court now, Charting fans. So, what's it going to be then, eh?

Get out of the way with the tray ["Minimize to tray" sample implementation for WPF]

"Minimize to tray" is a feature in some applications where minimizing the application removes its taskbar button and replaces it with a (much smaller) notification area icon. Clicking that notification icon restores the application's window - just like clicking its taskbar button would have done.

Well, I was considering using this functionality in a project I'm working on, so I looked to see what my options were. This feature didn't seem to be directly supported by WPF, so I searched the web a bit. What I found after a minute or two of searching was plenty of questions about how to implement this, a few suggestions, and not a lot else. So I figured I'd put something together myself and post it to my blog...

 

Here's the sample application I'm about to minimize - note the (boring) green square of an application icon:

About to minimize

And here's what things look like just after the application is minimized to the tray:

Notify balloon

Aside from just looking cool, that notification bubble serves an important purpose: it helps to draw the user's attention to the new icon in the notification area and calls out the application's custom minimize behavior. (To avoid being annoying, the bubble is only shown the first time the application is minimized each time it's run.) This is a nice convenience on most versions of Windows, but is pretty much necessary due to the new Windows 7 behavior of hiding notification area icons as quickly as possible. Without a helpful indication like this bubble, users might "lose" the application when it minimizes to the tray.

Aside: I understand why the Windows 7 team introduced this new behavior and I think it's perfectly reasonable. However, it has implications for this scenario, so it's good to think twice (or thrice!) about choosing to use "minimize to tray" in your application. The document I link to above has lots more guidance on the proper use of notification icons - interested parties are encouraged to review it!

After Windows 7 hides the notification icon for the application, it can be accessed via the "Show hidden icons" popup. Clicking on the hidden notification icon restores the application just like you'd expect:

Hidden by Windows 7
Aside: If you really dislike the new hiding behavior, it's easy to disable - just click the "Customize..." link (shown in the image above) and you'll be presented with a window that lets you disable this behavior for specific applications - or disable it for all of them with a single checkbox.

 

Notes:

  • I've implemented this functionality in a static MinimizeToTray class with a single Enable method that's super-easy to use. Just add a call to it in your Window's constructor and you're done:
    // Enable "minimize to tray" behavior for this Window
    MinimizeToTray.Enable(this);
    
  • WPF doesn't natively offer notify icon support, but there's nothing stopping us from using the NotifyIcon implementation that's part of WinForms! That's what MinimizeToTray does, so it's important to note that you'll need to change your project to add references to the System.Drawing and System.Windows.Forms .NET assemblies. (FYI, they're both part of the .NET Framework and are already present in the GAC (and NGEN-ed), so you don't need to worry about distributing them with your application.)
  • But it's also important to note that I've written my code such that MinimizeToTray doesn't cause either of these assemblies get loaded until the user first minimizes the application. This means neither assembly will impact the startup time of your application! (You can verify this by running the sample application, attaching the debugger, checking the module list to see that neither assembly is present, minimizing the application, and then noting that both assemblies just got loaded.)
  • I only use the most basic notify icon functionality in this sample, so the WinForms implementation is more than adequate for my needs. However, if you're looking for true WPF-style notification icon support, this implementation by Philipp Sumi looks quite promising (though I haven't tried it myself).

 

As a final favor, I'll ask that people please use "minimize to tray" wisely - just because you can minimize your application to the tray doesn't mean you should. :) Minimizing to the tray is something that's only meaningful for a limited set of scenarios - but if you find yourself in one of them, I hope MinimizeToTray is helpful!

 

[Click here to download the sample application and complete source code for MinimizeToTray.]

 

The code is quite straightforward - here it is in its entirety:

/// <summary>
/// Class implementing support for "minimize to tray" functionality.
/// </summary>
public static class MinimizeToTray
{
    /// <summary>
    /// Enables "minimize to tray" behavior for the specified Window.
    /// </summary>
    /// <param name="window">Window to enable the behavior for.</param>
    public static void Enable(Window window)
    {
        // No need to track this instance; its event handlers will keep it alive
        new MinimizeToTrayInstance(window);
    }

    /// <summary>
    /// Class implementing "minimize to tray" functionality for a Window instance.
    /// </summary>
    private class MinimizeToTrayInstance
    {
        private Window _window;
        private NotifyIcon _notifyIcon;
        private bool _balloonShown;

        /// <summary>
        /// Initializes a new instance of the MinimizeToTrayInstance class.
        /// </summary>
        /// <param name="window">Window instance to attach to.</param>
        public MinimizeToTrayInstance(Window window)
        {
            Debug.Assert(window != null, "window parameter is null.");
            _window = window;
            _window.StateChanged += new EventHandler(HandleStateChanged);
        }

        /// <summary>
        /// Handles the Window's StateChanged event.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event arguments.</param>
        private void HandleStateChanged(object sender, EventArgs e)
        {
            if (_notifyIcon == null)
            {
                // Initialize NotifyIcon instance "on demand"
                _notifyIcon = new NotifyIcon();
                _notifyIcon.Icon = Icon.ExtractAssociatedIcon(Assembly.GetEntryAssembly().Location);
                _notifyIcon.MouseClick += new MouseEventHandler(HandleNotifyIconOrBalloonClicked);
                _notifyIcon.BalloonTipClicked += new EventHandler(HandleNotifyIconOrBalloonClicked);
            }
            // Update copy of Window Title in case it has changed
            _notifyIcon.Text = _window.Title;

            // Show/hide Window and NotifyIcon
            var minimized = (_window.WindowState == WindowState.Minimized);
            _window.ShowInTaskbar = !minimized;
            _notifyIcon.Visible = minimized;
            if (minimized && !_balloonShown)
            {
                // If this is the first time minimizing to the tray, show the user what happened
                _notifyIcon.ShowBalloonTip(1000, null, _window.Title, ToolTipIcon.None);
                _balloonShown = true;
            }
        }

        /// <summary>
        /// Handles a click on the notify icon or its balloon.
        /// </summary>
        /// <param name="sender">Event source.</param>
        /// <param name="e">Event arguments.</param>
        private void HandleNotifyIconOrBalloonClicked(object sender, EventArgs e)
        {
            // Restore the Window
            _window.WindowState = WindowState.Normal;
        }
    }
}
Tags: WPF

Using your own code is its own kind of encouragement [How to: Automatically update the widths of ListView columns - updated!]

Some time ago, I blogged about a helper class to automatically adjust the size of the columns of a ListView's GridView to fit their contents. In my scenario, I was changing the value of the ListView's ItemsSource property and wanted the columns to update each time to accommodate the new data. The solution I described and posted at that time has been working fine for me ever since. However, a new project has just revealed a shortcoming: if the collection is one that implements INotifyCollectionChanged (such as ObservableCollection<T>), its contents will change and provide notice they've done so, but the column widths won't - because the original implementation only cared about changes to the ItemsSource property itself...

I've updated sample application I wrote for the previous post to include a new "Add Detail" button that dynamically adds an item to the current collection. As you can see from the image below, the second ListView (which doesn't take advantage of the new code) hasn't expanded the widths of its two columns in response to the new item. However, the bottom ListView which uses the IsAutoUpdatingColumnWidths attached DependencyProperty has automatically detected the new item and updated itself accordingly:

ListViewColumnWidthAutoUpdate sample

[Click here to download the ListViewColumnWidthAutoUpdate sample application.]

 

The new code seemed like it should be easy enough, then turned out to be a bit more involved than I expected. But the important point is that if you're already using IsAutoUpdatingColumnWidths, then you don't need to change your code at all! Just drop in the new implementation of my ListViewBehaviors class, and you're set!

 

Notes:

  • At the end of the day, this update is really just about detecting that the ItemsSource collection implements INotifyCollectionChanged and listening to its CollectionChanged event. The complications arise because IsAutoUpdatingColumnWidths is an attached DependencyProperty and therefore static - so there's no convenient place to associate instance-specific state. In particular, the necessary state is a list of collection-owning ListViews for reverse-mapping and a reference to the previous collection instance because the handler that DependencyPropertyDescriptor.AddValueChanged calls when a value changes only gets the new value (unlike the PropertyChangedCallback that's typically associated with DependencyPropertys).
  • So I introduced a simple ListViewState helper class and some code to maintain a list of known ListView instances an their associated INotifyCollectionChanged collections.
  • But we don't want to create "false" references to user objects and introduce leaks, so both of ListViewState's properties are backed by a WeakReference to allow them to be garbage collected when possible.
  • At this point, you might wonder if it's necessary to employ the WeakEvent pattern (which I've previously written about here)... Well, for now I've managed to convince myself that it is not - because unlike the scenario I originally wrote about, in this case the "backwards references" are all to a single static method. The way I'm thinking about it right now, that method can't really "leak" - or if it can, its size and scope are small enough that they're not worth worrying about. That said, I'm open to being proven wrong, so please don't be shy about correcting me if you know better! :)
  • The only other detail worth calling out is that it's necessary to hook and unhook the INotifyCollectionChanged event in two circumstances: when the ItemsSource property changes and when the IsAutoUpdatingColumnWidths property changes.

 

And that's all there is to it! Here's the complete, updated implementation of ListViewBehaviors:

/// <summary>
/// Class implementing useful behaviors for the ListView control.
/// </summary>
public static class ListViewBehaviors
{
    /// <summary>
    /// Updates the column widths of a GridView to fit the contents.
    /// </summary>
    /// <param name="gridView">GridView to update.</param>
    public static void UpdateColumnWidths(GridView gridView)
    {
        // Validate parameter
        Debug.Assert(null != gridView,
            "UpdateColumnWidths requires a non-null gridView parameter.");

        // For each column...
        foreach (var column in gridView.Columns)
        {
            // If this is an "auto width" column...
            if (double.IsNaN(column.Width))
            {
                // Set its Width back to NaN so it will auto-size
                column.Width = 0;
                column.Width = double.NaN;
            }
        }
    }

    /// <summary>
    /// Represents the IsAutoUpdatingColumnWidths attached DependencyProperty.
    /// </summary>
    public static readonly DependencyProperty IsAutoUpdatingColumnWidthsProperty =
        DependencyProperty.RegisterAttached(
            "IsAutoUpdatingColumnWidths",
            typeof(bool),
            typeof(ListViewBehaviors),
            new UIPropertyMetadata(false, OnIsAutoUpdatingColumnWidthsChanged));

    /// <summary>
    /// Gets the value of the IsAutoUpdatingColumnWidths property.
    /// </summary>
    /// <param name="listView">ListView for which to get the value.</param>
    /// <returns>Value of the property.</returns>
    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
        Justification = "Only applies to ListView instances.")]
    public static bool GetIsAutoUpdatingColumnWidths(ListView listView)
    {
        return (bool)listView.GetValue(IsAutoUpdatingColumnWidthsProperty);
    }

    /// <summary>
    /// Sets the value of the IsAutoUpdatingColumnWidths property.
    /// </summary>
    /// <param name="listView">ListView for which to set the value.</param>
    /// <param name="value">Value of the property.</param>
    [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters",
        Justification = "Only applies to ListView instances.")]
    public static void SetIsAutoUpdatingColumnWidths(ListView listView, bool value)
    {
        listView.SetValue(IsAutoUpdatingColumnWidthsProperty, value);
    }

    /// <summary>
    /// Change handler for the IsAutoUpdatingColumnWidths property.
    /// </summary>
    /// <param name="o">Instance for which it changed.</param>
    /// <param name="e">Change details.</param>
    private static void OnIsAutoUpdatingColumnWidthsChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        // Get the ListView instance and new bool value
        var listView = o as ListView;
        Debug.Assert(!VirtualizingStackPanel.GetIsVirtualizing(listView),
            "VirtualizingStackPanel.IsVirtualizing should be False for " +
            "ListViewBehaviors.IsAutoUpdatingColumnWidths to work best.");
        if ((null != listView) && (e.NewValue is bool))
        {
            // Get a descriptor for the ListView's ItemsSource property
            var descriptor = DependencyPropertyDescriptor.FromProperty(ListView.ItemsSourceProperty, typeof(ListView));
            if ((bool)e.NewValue)
            {
                // Enabling the feature, so add the change handler
                descriptor.AddValueChanged(listView, ListViewItemsSourceValueChanged);

                // Create a ListViewState instance for tracking
                var listViewState = new ListViewState(listView);

                // Hook the CollectionChanged event (if possible)
                var iNotifyCollectionChanged = listView.ItemsSource as INotifyCollectionChanged;
                if (null != iNotifyCollectionChanged)
                {
                    iNotifyCollectionChanged.CollectionChanged +=
                        new NotifyCollectionChangedEventHandler(ListViewItemsSourceCollectionChanged);
                    listViewState.INotifyCollectionChanged = iNotifyCollectionChanged;
                }

                // Track the instance
                _listViewStates.Add(listViewState);
            }
            else
            {
                // Disabling the feature, so remove the change handler
                descriptor.RemoveValueChanged(listView, ListViewItemsSourceValueChanged);

                // Look up the corresponding ListViewState instance
                var listViewState = _listViewStates
                    .Where(lvs => listView == lvs.ListView)
                    .FirstOrDefault();
                if (null != listViewState)
                {
                    // Unhook the CollectionChanged event (if present)
                    var iNotifyCollectionChanged = listViewState.INotifyCollectionChanged;
                    if (null != iNotifyCollectionChanged)
                    {
                        iNotifyCollectionChanged.CollectionChanged -=
                            new NotifyCollectionChangedEventHandler(ListViewItemsSourceCollectionChanged);
                        listViewState.INotifyCollectionChanged = null;
                    }
                }

                // Remove the ListViewState (and any other unnecessary ones)
                foreach (var lvs in _listViewStates
                    .Where(lvs => (listView == lvs.ListView) || (null == lvs.ListView))
                    .ToArray()) // ToArray avoids modifying the current collection
                {
                    _listViewStates.Remove(lvs);
                }
            }
        }
    }

    /// <summary>
    /// Handles changes to the ListView's ItemsSource and updates the column widths.
    /// </summary>
    /// <param name="sender">Event source.</param>
    /// <param name="e">Event args.</param>
    private static void ListViewItemsSourceValueChanged(object sender, EventArgs e)
    {
        // Get a reference to the ListView
        var listView = sender as ListView;
        if (null != listView)
        {
            // Look up the corresponding ListViewState instance
            var listViewState = _listViewStates
                .Where(lvs => listView == lvs.ListView)
                .FirstOrDefault();
            if (null != listViewState)
            {
                // Unhook the CollectionChanged event (if present)
                var oldINotifyCollectionChanged = listViewState.INotifyCollectionChanged;
                if (null != oldINotifyCollectionChanged)
                {
                    oldINotifyCollectionChanged.CollectionChanged -=
                        new NotifyCollectionChangedEventHandler(ListViewItemsSourceCollectionChanged);
                    listViewState.INotifyCollectionChanged = null;
                }

                // Hook the new CollectionChanged event (if possible)
                var newINotifyCollectionChanged = listView.ItemsSource as INotifyCollectionChanged;
                if (null != newINotifyCollectionChanged)
                {
                    newINotifyCollectionChanged.CollectionChanged +=
                        new NotifyCollectionChangedEventHandler(ListViewItemsSourceCollectionChanged);
                    listViewState.INotifyCollectionChanged = newINotifyCollectionChanged;
                }

                // Get a reference to the ListView's GridView
                var gridView = listView.View as GridView;
                if (null != gridView)
                {
                    // Update the ListView's column widths
                    UpdateColumnWidths(gridView);
                }
            }
        }
    }

    /// <summary>
    /// Handles changes to the ListView's ItemsSource's CollectionChanged event and updates the column widths.
    /// </summary>
    /// <param name="sender">Event source.</param>
    /// <param name="e">Event args.</param>
    private static void ListViewItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // Update the corresponding GridView by looking up the ListView by ItemsSource
        foreach (var gridView in _listViewStates
                .Select(lvs => lvs.ListView)
                .Where(lv => (null != lv) && (sender == lv.ItemsSource) && (lv.View is GridView))
                .Select(lv => (GridView)(lv.View)))
        {
            // Update the ListView's column widths
            UpdateColumnWidths(gridView);
        }
    }

    /// <summary>
    /// Stores a collection of ListViewState instances.
    /// </summary>
    private static List<ListViewState> _listViewStates = new List<ListViewState>();

    /// <summary>
    /// Stores state information about a ListView that allows ListViewItemsSourceCollectionChanged
    /// to map from a collection to the corresponding ListView owner.
    /// </summary>
    private class ListViewState
    {
        /// <summary>
        /// Weakly references the ListView.
        /// </summary>
        private WeakReference _listViewReference = new WeakReference(null);

        /// <summary>
        /// Gets or sets the ListView.
        /// </summary>
        public ListView ListView
        {
            get { return (ListView)(_listViewReference.Target); }
            private set { _listViewReference.Target = value; }
        }

        /// <summary>
        /// Weakly references the INotifyCollectionChanged.
        /// </summary>
        private WeakReference _iNotifyCollectionChangedReference = new WeakReference(null);

        /// <summary>
        /// Gets or sets the INotifyCollectionChanged.
        /// </summary>
        public INotifyCollectionChanged INotifyCollectionChanged
        {
            get { return (INotifyCollectionChanged)(_iNotifyCollectionChangedReference.Target); }
            set { _iNotifyCollectionChangedReference.Target = value; }
        }

        /// <summary>
        /// Creates a new instance of the ListViewState class.
        /// </summary>
        /// <param name="listView">Corresponding ListView.</param>
        public ListViewState(ListView listView)
        {
            ListView = listView;
        }
    }
}
Tags: WPF

Scrolling so smooth like the butter on a muffin [How to: Animate the Horizontal/VerticalOffset properties of a ScrollViewer]

Recently, I got email from two different people with the same question: How do I animate the HorizontalOffset/VerticalOffset properties of a Silverlight/WPF ScrollViewer control? Just in case you've never tried this yourself, I'll tell you that it's not quite as easy as you'd think; those two properties are both read-only and therefore can't be animated by a DoubleAnimation. The official way to accomplish this task is to call the ScrollToHorizontalOffset or ScrollToVerticalOffset method - but of course that can't be done by a DoubleAnimation either...

Aside: Why are HorizontalOffset and VerticalOffset read-only in the first place? Good question; I don't know! I don't see why they couldn't be writable in today's world, but I bet there was a good reason - once upon a time. :)

While I didn't have the time to implement a solution myself, I did have an idea for what to do. It was inspired by a rather quirky business plan and went like this:

  1. Create an attached DP for ScrollViewer of type double called SettableHorizontalOffset
  2. In its change handler, call scrollViewerInstance.ScrollToHorizontalOffset(newValue)
  3. Animate the attached DP on the ScrollViewer of your choice
  4. ???
  5. Profit

I cautioned at the time that my idea might not work - and sure enough, when I tried it myself, it didn't! It turns out that neither Silverlight nor WPF much like the idea of pointing Storyboard.TargetProperty at an attached DependencyProperty. So much for my clever idea; I needed a different way to solve the problem... And before long I realized that I could use a Mediator just like I did for a similar situation with LayoutTransformer (see previous posts).

 

So I created a simple class, ScrollViewerOffsetMediator, which exposes a ScrollViewer property that you point at a ScrollViewer instance (hint: use a Binding with ElementName to make this easy) and a VerticalOffset property which is conveniently writable. :) Any changes to the VerticalOffset property are automatically applied to the relevant ScrollViewer via a call to its ScrollToVerticalOffset method - so you can animate the mediator and it'll be just like you were animating the ScrollViewer itself!

That solves the original problem and it's all well and good - but it doesn't quite address a pretty common scenario: wanting to animate a ScrollViewer from top to bottom. You see, VerticalOffset is expressed in pixels and it's not easy to pass the (unknown) height of an arbitrary ScrollViewer to an animation in static XAML. So I added another property called ScrollableHeightMultiplier which takes a value between 0.0 and 1.0, multiplies it by the ScrollViewer's current ScrollableHeight and sets the VerticalOffset to that value. In this manner, animations can be written in general, pixel-independent terms, and it's easy to get the animated scrolling effect everybody wants!

Aside: I've implemented support for only the vertical direction - doing the same thing for the horizontal direction is left as an exercise to the reader.

 

The sample application uses ScrollableHeightMultiplier and an EasingFunction (just to show off!) and looks like this:

AnimatingScrollViewerOffsets sample

 

[Click here to download the complete AnimatingScrollViewerOffsets sample.]

 

The XAML for the sample is quite straightforward:

<Grid Height="200" Width="150">

    <!-- Trigger to start the animation when loaded -->
    <Grid.Triggers>
        <EventTrigger RoutedEvent="Grid.Loaded">
            <BeginStoryboard>
                <BeginStoryboard.Storyboard>
                    <!-- Animate back and forth forever -->
                    <Storyboard
                        AutoReverse="True"
                        RepeatBehavior="Forever">
                        <!-- Animate from top to bottom -->
                        <DoubleAnimation
                            Storyboard.TargetName="Mediator"
                            Storyboard.TargetProperty="ScrollableHeightMultiplier"
                            From="0"
                            To="1"
                            Duration="0:0:1">
                            <DoubleAnimation.EasingFunction>
                                <!-- Ease in and out -->
                                <ExponentialEase EasingMode="EaseInOut"/>
                            </DoubleAnimation.EasingFunction>
                        </DoubleAnimation>
                    </Storyboard>
                </BeginStoryboard.Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Grid.Triggers>

    <!-- ScrollViewer that will be animated -->
    <ScrollViewer
        x:Name="Scroller">
        <!-- Arbitrary content... -->
        <StackPanel>
            <ItemsControl
                ItemsSource="{Binding}"
                FontSize="32"/>
        </StackPanel>
    </ScrollViewer>

    <!-- Mediator that forwards the property changes -->
    <local:ScrollViewerOffsetMediator
        x:Name="Mediator"
        ScrollViewer="{Binding ElementName=Scroller}"/>

</Grid>

 

Here's the complete implementation of ScrollViewerOffsetMediator which naturally works perfectly well on WPF:

/// <summary>
/// Mediator that forwards Offset property changes on to a ScrollViewer
/// instance to enable the animation of Horizontal/VerticalOffset.
/// </summary>
public class ScrollViewerOffsetMediator : FrameworkElement
{
    /// <summary>
    /// ScrollViewer instance to forward Offset changes on to.
    /// </summary>
    public ScrollViewer ScrollViewer
    {
        get { return (ScrollViewer)GetValue(ScrollViewerProperty); }
        set { SetValue(ScrollViewerProperty, value); }
    }
    public static readonly DependencyProperty ScrollViewerProperty =
        DependencyProperty.Register(
            "ScrollViewer",
            typeof(ScrollViewer),
            typeof(ScrollViewerOffsetMediator),
            new PropertyMetadata(OnScrollViewerChanged));
    private static void OnScrollViewerChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var mediator = (ScrollViewerOffsetMediator)o;
        var scrollViewer = (ScrollViewer)(e.NewValue);
        if (null != scrollViewer)
        {
            scrollViewer.ScrollToVerticalOffset(mediator.VerticalOffset);
        }
    }

    /// <summary>
    /// VerticalOffset property to forward to the ScrollViewer.
    /// </summary>
    public double VerticalOffset
    {
        get { return (double)GetValue(VerticalOffsetProperty); }
        set { SetValue(VerticalOffsetProperty, value); }
    }
    public static readonly DependencyProperty VerticalOffsetProperty =
        DependencyProperty.Register(
            "VerticalOffset",
            typeof(double),
            typeof(ScrollViewerOffsetMediator),
            new PropertyMetadata(OnVerticalOffsetChanged));
    public static void OnVerticalOffsetChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var mediator = (ScrollViewerOffsetMediator)o;
        if (null != mediator.ScrollViewer)
        {
            mediator.ScrollViewer.ScrollToVerticalOffset((double)(e.NewValue));
        }
    }

    /// <summary>
    /// Multiplier for ScrollableHeight property to forward to the ScrollViewer.
    /// </summary>
    /// <remarks>
    /// 0.0 means "scrolled to top"; 1.0 means "scrolled to bottom".
    /// </remarks>
    public double ScrollableHeightMultiplier
    {
        get { return (double)GetValue(ScrollableHeightMultiplierProperty); }
        set { SetValue(ScrollableHeightMultiplierProperty, value); }
    }
    public static readonly DependencyProperty ScrollableHeightMultiplierProperty =
        DependencyProperty.Register(
            "ScrollableHeightMultiplier",
            typeof(double),
            typeof(ScrollViewerOffsetMediator),
            new PropertyMetadata(OnScrollableHeightMultiplierChanged));
    public static void OnScrollableHeightMultiplierChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
        var mediator = (ScrollViewerOffsetMediator)o;
        var scrollViewer = mediator.ScrollViewer;
        if (null != scrollViewer)
        {
            scrollViewer.ScrollToVerticalOffset((double)(e.NewValue) * scrollViewer.ScrollableHeight);
        }
    }
}

 

PS - Thanks to Homestar Runner's Strong Bad for inspiring the title of this post.

Simple column labels you can create at home! [Re-Templating the Silverlight/WPF Data Visualization ColumnDataPoint to add annotations]

A customer contacted me over the weekend asking how to add labels (also known as annotations) to a ColumnSeries. My reply was that we don't support annotations in Silverlight/WPF Charting yet, but it's possible to create some pretty simple ones for limited scenarios just by editing the default ColumnDataPoint Template. And because it's so quick, I thought I'd write up a brief example on the bus!

Aside: For some more examples of basic DataPoint Template changes, please have a look at my earlier post on customizing ToolTips.

 

In this case, the customer wanted to add a label showing the column's value at the bottom of the column, just above its axis label. (This is a nice place to put labels because it makes it easy for viewers to associate the category with its value no matter how high each column is.) The obvious approach is to add a TextBlock to the body of the default Template, but the problem with that is that the text can get clipped or even disappear for small columns... So the trick is to add a negative Margin to pull the text "outside" the normal clipping region. Fortune must have been smiling upon me, because when I tried this on Silverlight, it worked just like I wanted! :)

Aside: My other idea was to use a Canvas because it doesn't clip by default; maybe someone else will need to use that approach for their scenario.

 

Here's how the resulting chart looks:

Simple column annotations (on bottom)

The XAML's nothing special - aside from the negative Margin, it's all standard stuff:

<charting:Chart
    Title="Simple Column Annotations - Bottom">
    <charting:ColumnSeries
        DependentValuePath="Value"
        IndependentValuePath="Key"
        ItemsSource="{Binding}">
        <charting:ColumnSeries.DataPointStyle>
            <Style TargetType="charting:ColumnDataPoint">
                <Setter Property="Background" Value="Yellow"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="charting:ColumnDataPoint">
                            <Grid>
                                <Rectangle
                                    Fill="{TemplateBinding Background}"
                                    Stroke="Black"/>
                                <Grid
                                    Background="#aaffffff"
                                    Margin="0 -20 0 0"
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Bottom">
                                    <TextBlock
                                        Text="{TemplateBinding FormattedDependentValue}"
                                        FontWeight="Bold"
                                        Margin="2"/>
                                </Grid>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </charting:ColumnSeries.DataPointStyle>
    </charting:ColumnSeries>
</charting:Chart>

 

Just for fun, I thought I'd try the same trick to put the annotations on top of the columns, too. This is the more traditional location - and that also works pretty nicely:

Simple column annotations (on top)

The only change from the previous XAML is switching the VerticalAlignment to Top:

<Grid
    Background="#aaffffff"
    Margin="0 -20 0 0"
    HorizontalAlignment="Center"
    VerticalAlignment="Top">
    <TextBlock
        Text="{TemplateBinding FormattedDependentValue}"
        FontWeight="Bold"
        Margin="2"/>
</Grid>

 

And there you have it - a simple technique for simple column annotations!

Aside: Of course, these aren't "real" annotations - they'll eventually break in more complicated scenarios. But hey, if you've got simple needs, here's a simple solution for you. :)

 

PS - For people playing along at home, here's how I created the data for the samples:

public MainPage()
{
    InitializeComponent();
    var items = new List<KeyValuePair<string, double>>();
    items.Add(new KeyValuePair<string,double>("Apples", 0));
    items.Add(new KeyValuePair<string,double>("Oranges", 0.1));
    items.Add(new KeyValuePair<string,double>("Pears", 1));
    DataContext = items;
}

My new home page, enhanced [Updated collection of great Silverlight/WPF Data Visualization resources!]

In the time since posting my last collection of Silverlight/WPF Charting links, there's been some great activity! Of particular significance, the June 2009 WPF Toolkit includes the same Data Visualization goodness that was introduced for Silverlight and the July 2009 release of the Silverlight Toolkit added a TreeMap control. Without further ado, here are the latest links (FYI: previously published links are gray):

Overviews (100 level)

Scenarios (200 level)

Internals (300 level)

Team Member posts (Partner level)

My posts (Ego level)

Many, many thanks to everyone who's spent time helping others learn how to use Silverlight/WPF Data Visualization!

PS - If I've missed any good resources, please leave a comment with a link - I'm always happy to find more good stuff! :)