Wednesday, October 16, 2013

Extending XamDataGrid to Support Zooming

Zoomable views have become commonplace in many applications. The ability to arbitrarily zoom in and out is a great way to cater to different screen sizes and viewing scenarios. Unfortunately for me, they have become so common that my boss decided our in-development UI needed the same capabilities. Fortunately for me, we were developing the UI in WPF, so adding zoomable controls isn't a big deal because of the LayoutTransform. The UI we were developing had a lot of spreadsheet type views that were implemented with the Infragistics XamDataGrid, so adding zoomable support to this control was a must.

Adding the ability to zoom within the XamDataGrid has the following requirements.

  1. The XamDataGrid cells and column headers must scale to match the current zoom value.
  2. There must be an editable combo box that contains the current zoom value.
  3. The editable combo box must reside in the horizontal scrollbar on the right side.
  4. Ctrl + mouse wheel must zoom in and out by a set percentage when the mouse is within the boundaries of the XamDataGrid.

MouseWheelZooming Attached Properties

I like using attached properties to tack functionality onto already existing controls. It's quick, easy, and malleable. For this feature addition, I need the control to maintain a zoom value, as well as automatically increase/decrease that value on mouse wheel events. For this, I created a MouseWheelZooming class to house the necessary attached properties.

A IsEnabledProperty will determine if the zooming is in use, attaching event handlers when enabled and changing the zoom values when the wheel events occur. The following code is the implementation for the IsEnabledProperty.
public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(MouseWheelZooming),
                                                                                                          new FrameworkPropertyMetadata(false)
                                                                                                          {
                                                                                                              PropertyChangedCallback = ZoomEnabledChanged,
                                                                                                              BindsTwoWayByDefault = true
                                                                                                          });
public static bool GetIsEnabled(DependencyObject item)
{
    return (bool)item.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject item, bool value)
{
    item.SetValue(IsEnabledProperty, value);
}

private static void ZoomEnabledChanged(DependencyObject s, DependencyPropertyChangedEventArgs args)
{
    Control c = (Control)s;
    bool wasEnabled = (bool)args.OldValue;
    bool isEnabled = (bool)args.NewValue;

    if (wasEnabled)
        c.RemoveHandler(Control.PreviewMouseWheelEvent, new MouseWheelEventHandler(PreviewMouseWheel));

    if (isEnabled)
        c.AddHandler(Control.PreviewMouseWheelEvent, new MouseWheelEventHandler(PreviewMouseWheel), true);
}

private static void PreviewMouseWheel(Object sender, MouseWheelEventArgs e)
{
    if (Keyboard.Modifiers == ModifierKeys.Control)
    {
        Control zoomableControl = (Control)sender;

        double zoomAmount = (double)zoomableControl.GetValue(ZoomAmountProperty);
        double zoomMinimum = (double)zoomableControl.GetValue(ZoomMinimumProperty);
        double zoomMaximum = (double)zoomableControl.GetValue(ZoomMaximumProperty);

        if (e.Delta > 0)
            zoomAmount *= 1.1;
        else
            zoomAmount *= 0.90909090909090906;

        if (zoomAmount > zoomMaximum)
            zoomAmount = zoomMaximum;
        else if (zoomAmount < zoomMinimum)
            zoomAmount = zoomMinimum;

        zoomAmount = Math.Round(zoomAmount, 2);

        zoomableControl.SetValue(ZoomAmountProperty, zoomAmount);
    }
}
Note that this code references ZoomAmountProperty, ZoomMinimumProperty, and ZoomMaximumProperty. These are additional attached properties in my MouseWheelZooming class, but the declarations are omitted here because they are strictly boiler plate.

That's it. I now have my zoom value, which is also automatically changed by the mouse wheel. Now I just need to hook up the zoom value to the control scaling.

LayoutTransform

As mentioned, I used the LayoutTransform to perform all scaling. All FrameworkElements have two types of transforms.
  1. LayoutTransform - This transform is applied before the layout pass. The layout system will work with the transformed size and location when laying out the UI.
  2. RenderTransform - This transform is applied after the layout pass but before the render. Ideal when changes are independent of the rest of the UI because of performance gains.
I use the LayoutTransform because the size changes need to remain sensible within the UI. For example, scaling a grid to 200% shouldn't overlap other items in the UI. Instead, it should expand as far as it's parent allows, and then expose scrollbars if the size exceeds the allowed space. The LayoutTransform provides this functionality.

The transform used to achieve the scaling is a very simple one. It's just a ScaleTransform that uses a common value for the ScaleX and ScaleY to provide fixed ratio scaling. The only problem is that we're using XamDataGrid, which is a bit more sophisticated than more controls. We can't just scale XamDataGrid and expect everything to come out nicely.

Scaling XamDataGrid

When scaling is applied directly to XamDataGrid you end up with scrollbars that scroll the column headers with the cells. All data grid implementations I'm familiar with scroll only the rows, and the column headers remain static at the top of the view. Having scrolling headers is terrible experience for end users. The scaling has to be done at a deeper level within XamDataGrid.

To get proper grid scaling, the column headers and cells need to be scaled independently so that the layout logic will continue to function as designed (leaving the headers at the top). Luckily, this is possible though the use of styles. The XamDataGrid lets you set the style for various components within the grid through FieldLayoutSettings. This class has two properties on it that we'll use.
  1. DataRecordCellAreaStyle - The style applied to each record's cell area.
  2. HeaderLabelAreaStyle - The style applied to the header's label area.
By applying scaling through these styles, I can change the size of the inner components of the grid and still allow the grid's layout logic to work normally, giving correct functionality when scrolling is necessary (the headers will not scroll). Both properties can use the same style.
<Style x:Key="ZoomTransform" TargetType="{x:Type Control}">
    <Setter Property="LayoutTransform">
        <Setter.Value>
            <ScaleTransform ScaleX="{Binding RelativeSource={RelativeSource AncestorType=igData:XamDataGrid}, Path=(vibrantUiWpf:MouseWheelZooming.ZoomAmount)}" 
                                            ScaleY="{Binding RelativeSource={RelativeSource AncestorType=igData:XamDataGrid}, Path=(vibrantUiWpf:MouseWheelZooming.ZoomAmount)}" />
        </Setter.Value>
    </Setter>
</Style>
This style uses relative binding to reach up to the parent XamDataGrid and obtain the zoom amount. Recall that ZoomAmount is the attached property I created, which is why the syntax for the path is different than typical path syntax. Now when creating a XamDataGrid, the FieldLayoutSettings can be set as such.
<igData:XamDataGrid.FieldLayoutSettings>
    <igData:FieldLayoutSettings DataRecordCellAreaStyle="{StaticResource ZoomTransform}" HeaderLabelAreaStyle="{StaticResource ZoomTransform}" />
</igData:XamDataGrid.FieldLayoutSettings>
The XamDataGrid can now be zoomed with the crtl+mouse wheel, or by setting the value of the ZoomAmount attached property.
XamDataGrid at 50%

XamDataGrid at 100%
XamDataGrid at 200%

ScrollViewer with ComboBox

With the zooming now functional, all that remains is displaying the current zoom value in the UI. Recall from #2 and #3 from the 4 requirements. To achieve this, I need a combo box that will reside adjacent to the horizontal scrollbar. The XamDataGrid is built with a ScrollViewer, so to customize this, I created a ControlTemplate for a ScrollViewer that includes the combo box.
<ControlTemplate x:Key="ZoomableScrollViewer" TargetType="ScrollViewer">
    <ControlTemplate.Resources>
        <Style TargetType="ScrollViewer" />
    </ControlTemplate.Resources>
    <Grid Background="{TemplateBinding Background}" Tag="{Binding}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <ScrollBar
                x:Name="PART_VerticalScrollBar"
                Grid.Column="2"
                Minimum="0.0"
                Maximum="{TemplateBinding ScrollableHeight}"
                ViewportSize="{TemplateBinding ViewportHeight}"
                Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=VerticalOffset, Mode=OneWay}"
                Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"         
                Cursor="Arrow"
                AutomationProperties.AutomationId="VerticalScrollBar"/>
        <ScrollBar
                x:Name="PART_HorizontalScrollBar"
                Orientation="Horizontal"
                Grid.Row="1"
                Grid.Column="1"
                Minimum="0.0"
                Maximum="{TemplateBinding ScrollableWidth}"
                ViewportSize="{TemplateBinding ViewportWidth}"
                Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=HorizontalOffset, Mode=OneWay}"
                Visibility="Visible"
                Cursor="Arrow"
                AutomationProperties.AutomationId="HorizontalScrollBar"/>
        <local:EditableComboBox Width="50" Grid.Column="0" Grid.Row="1" IsEditable="True" FontSize="11" Padding="0"
                                                    Text="{Binding RelativeSource={RelativeSource AncestorType=igData:XamDataGrid}, Path=(local:MouseWheelZooming.ZoomAmount), 
                                                            Mode=TwoWay, UpdateSourceTrigger=Explicit, Converter={StaticResource PercentageConverter}, ConverterParameter=%}">
                    <local:EditableComboBox.ItemsSource>
                        <x:Array Type="{x:Type sys:Double}">
                            <sys:Double&gt20</sys:Double>
                            <sys:Double&gt50</sys:Double>
                            <sys:Double&gt70</sys:Double>
                            <sys:Double&gt100</sys:Double>
                            <sys:Double&gt150</sys:Double>
                            <sys:Double&gt200</sys:Double>
                            <sys:Double&gt400</sys:Double>
                        </x:Array>
                    </local:EditableComboBox.ItemsSource>
                    <local:EditableComboBox.ItemTemplate>
                        <DataTemplate>
                            <TextBlock>
                            <TextBlock.Inlines>
                                 <Run Text="{Binding Mode=OneWay}" />
                                 <Run Text="%" />
                            </TextBlock.Inlines>
                            </TextBlock>
                        </DataTemplate>
                    </local:EditableComboBox.ItemTemplate>
        </local:EditableComboBox>
        <ScrollContentPresenter 
                x:Name="PART_ScrollContentPresenter"
                Grid.ColumnSpan="2"
                Margin="{TemplateBinding Padding}"
                Content="{TemplateBinding Content}"
                ContentTemplate="{TemplateBinding ContentTemplate}"
                CanContentScroll="{TemplateBinding CanContentScroll}"/>
        <Rectangle 
                x:Name="Corner"
                Grid.Column="2"
                Grid.Row="1"
                Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
    </Grid>
</ControlTemplate>
I created this ControlTemplate from the default template for ScrollViewer. The main change is that I added the EditableComboBox to the grid layout (line #37). Note the relative source binding. This will enable the combo box to stay in sync with the current zoom value. I couldn't find a direct way to set this ControlTemplate with XamDataGrid, so I instead used a style to set the template.
<Style TargetType="igData:RecordListControl">
    <Style.Resources>
        <Style TargetType="ScrollViewer">
            <Setter Property="Template" Value="{StaticResource ZoomableScrollViewer}" />
        </Style>
    </Style.Resources>
</Style>
This will set my ControlTemplate on any ScrollViewer within the RecordListControl, which is the control used by XamDataGrid. That's all that is needed. The combo box will now display in the bottom corner.

XamDataGrid with full zooming capabilites

Source code: XamDataGrid.Zoomable

Sunday, June 2, 2013

WPF Manhattan Bar Chart - Tutorial #1

Introduction

3D graphics within a UI can often be gimmicky and fail to provide any real added insight. They look good when putting together a brochure, but users find these controls awkward and confusing. I've made several attempts to integrate 3D ideas into charts, and it rarely pays off. Users prefer the cleaner 2D interface. That said, 3D bar charts can be both functional and visually appealing. A simple 3D bar chart can be constructed with relative ease using the WPF 3D capabilities.

Over the next several tutorials, I will construct a Manhattan bar chart that will aim to be an easy to use WPF control that can be dropping into any project. These tutorials will cover the creation of the control itself, the data formatting, the 3D rendering, the labeling, and various other components that will aid in reusability.

Creating the control

To create this control, I've decided to use the Custom Control (WPF) template. I opted for this over the User Control (WPF) template for a couple reasons.
  1. A user control is generally used as a grouping of already existing controls. That's not what we're doing here.
  2. The custom control sets up the /Themes/Generic.xaml, which allows for a default style containing the ControlTemplate to be defined for the control we're creating.
Custom Control (WPF) can be found in the WPF templates.
To add the control, go to "Add New Item" within a C# project a select "Custom Control". Name the control "ManhattanBarChart.cs" and add it to the project. This will create the following class.
public class ManhattanBarChart : Control
{
    static ManhattanBarChart()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ManhattanBarChart), new FrameworkPropertyMetadata(typeof(ManhattanBarChart)));
    }
}

You can see that we've got our ManhattanBarChart class, and it inherits directly from Control. In WPF, a control is lookless by nature. To give it a visual, a ControlTemplate is necessary. So how do we define a ControlTemplate for our control? That's where theme styles come into play. A theme style is just like a regular style, except it's set implicitly. So by default, your control will have all properties set to the values indicated by the theme style. This is different than the Style property on the Control, which is known as the explicit style. Setting an explicit style will replace any values you set in the themes style with the settings defined on the explicit style.

So to give our control a visual, we need a themes style. Looking back at our class, notice that a static constructor has been created, and in it the DefaultStyleKeyProperty.OverrideMetadata method is called. Calling the OverrideMetadata method is a way of indicating which style should be used as the themes style for ManhattanBarChart. The default parameters set the themes style as the default style for type ManhattanBarChart. Now lets take a look at Generic.xaml, located in the Themes directory.
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ManhattanBarChartDemo">


    <Style TargetType="{x:Type local:ManhattanBarChart}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ManhattanBarChart}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

There is our default style, already created for us and ready for use. As mentioned before, to give the control a visual, Control.Template must be set to a ControlTemplate. This is done on lines 7-16. The default template is a simple border, with template bindings that will apply properties set on the ManhattanBarChart to the controls that make up the template. This ControlTemplate is where we'll do all of our XAML work.

Viewport3D

You may be familiar with drawing in WPF using the Canvas. The Canvas provides a simple way to draw almost anything within the specified boundaries. All you have to do is add the items to the Children collection on the Canvas, and it magically appears. It's great, but limited. The biggest limitation is that all drawing is 2D. Tapping into 3D drawing capabilites is a bit more invovled. But it's not all bad. WPF does provide some high level classes that make working within 3D space less painful that past experiences you may have had (working directly with Direct3D or OpenGL, for example).  The starting point for any 3D drawing within WPF is Viewport3D

Viewport3D inherits FrameworkElement, and therefore can be added anywhere within your UI. It's responsible for transforming your 3D scene into 2D space. Much like Canvas, it also has a Children collection. But this is a collection of Visual3D. These items make up the scene, but we'll take a closer look at that in Tutorial #2. For now, lets focus on the setup of the view.

Camera
Every viewport has a camera. Conceptually, the camera is exactly what it sounds like. If you take a camera, point it at a tree, and take a picture, you'll end up with a photograph of a tree. In that scenario, the viewport is the resulting photograph. It displays what's captured by the camera. There are three types of camera's that can be used.
  1. PerspectiveCamera - This is arguably the most widely used camera. It applies perspective to the scene so that objects further from the camera appear smaller. This gives depth, or perspective.
  2. OrthographicCamera - Much like the PerspectiveCamera, except no perspective is applied. Objects are the same size, regardless of distance from the camera.
  3. MatrixCamera - This is a more advanced camera that allows the developer to specify the individual view and projection matrices.
We'll be using PerspectiveCamera with our viewport. To setup the camera, we need to set a position within the scene, the direction that the camera is looking, and the up axis.

<PerspectiveCamera Position="0,0,1" LookDirection="0,0,-1" UpDirection="0,1,0" />
With these settings the camera is sitting at 0,0,1 in the scene and looking down the Z-axis at the scene origin. The up axis is set as the Y-axis (0,1,0), indicating the Y-axis is our vertical axis. That's all we have to do for now. This camera is ready for use.

Adding Viewport3D to the ControlTemplate

It's now time to add to our ControlTemplate that resided in Generic.xaml. Recall that this template will represent the how our control is viewed, as it's set within the default style. Lets add the Viewport3D we've been working on.
<ControlTemplate TargetType="{x:Type local:ManhattanBarChart}">
    <Border Background="{TemplateBinding Background}"
            BorderBrush="{TemplateBinding BorderBrush}"
            BorderThickness="{TemplateBinding BorderThickness}">
        <Viewport3D Name="viewport">
            <Viewport3D.Camera>
                <PerspectiveCamera Position="0,0,1" LookDirection="0,0,-1" UpDirection="0,1,0" />
            </Viewport3D.Camera>
        </Viewport3D>
    </Border>
</ControlTemplate>

Not much there yet, but we've got our Viewport3D and set the camera. Our control is setup and ready for 3D rendering!

In the next tutorial, we'll setup the rendering of the bars.

Source code for tutorial #1

Friday, January 11, 2013

Using Contains with an Entity DbSet

When using Entity, the DbSet is generally the gateway to your data. The great thing about that is that you can run LINQ queries on it and they will be converted to SQL, allowing you to selectively acquire data without pulling over the entire set. One thing I've found, however, is that because it must eventually translate to SQL, not everything LINQ can do is supported.

I had a case where I needed a list of only items added to the database since my last query. I wanted to use the Contains method as a way to determine what I already had. Consider the following example Context and Item class
class Context : DbContext
{
    public DbSet<Item> Items { get; set; }
}

class Item
{
    public int Id { get; set; }
}
With this context and a list of previously queried items (previousItems), I thought the following would work
context.Items.Where(x => !previousItems.Contains(x));
However when this query is executed, it throws a NotSupportedException with the following message:

"Unable to create a constant value of type 'Item'. Only primitive types ('such as Int32, String, and Guid') are supported in this context."

So what's going on here? What's this business about creating a constant value? I didn't tell it to do that. Well it turns out that's exactly what I told it to do. It comes back to the point made earlier about Entity converting the LINQ to SQL. Remember that this query is actually run against a database, so my Item object has no meaning. Entity wants to convert it to something SQL will understand, but it cannot. That's the reasoning behind only primitives being supported. The query must use types that SQL knows. With this in mind, I'll work on the Item IDs rather than the item itself.
var itemIds = previousItems.Select(x => x.Id);

var newItems = context.Items.Where(x => !itemIds.Contains(x.Id));
I created a list of the IDs from Items that I already have, and use that to compare against the database IDs to determine if I already have the item. Now that I'm working with type Int32 rather than Item, it works! The query runs and I get the correct results.

So how does the Contains method convert to SQL? The IN operator. My LINQ query ends up looking more like this when run against the database
SELECT * FROM items
WHERE Id NOT IN ( ... )
Pretty cool and very useful!