Sunday, December 15, 2013

Creating a non-rectangular user control

My new project has a consistent tree on the left of every page, a grid splitter, and a user control on the right. The user control changes depending on what the user is doing and each page has a custom set of buttons in a panel at the bottom. Unfortunately several pages have more buttons than can be fit across the bottom of the user control, especially when the page is displayed at 1024x768 resolution. I proposed using a wrap panel so the 1280x1024 users would see a single row and the 1024x768 users would see the buttons wrapped. But noooooooooooooo we have to do everything the hard way. The ladies that make these decisions want the button bar to spread across the entire screen. In other words they want a non-rectangular user control. Obviously this is impossible.

I could have added a second user control to the main page - one for the bulk of the controls and the other for the buttons, but wiring up routed commands between two different user controls is difficult and fragile. It also requires a lot of work at run time. So I decided to leverage negative margins. All I have to do is initialize the margin of the button panel at startup and also adjust it as the grid splitter moves. This turns out to require less than ten lines of code. Then the buttons are still inside the same user control as the rest of the page and no special wiring is required.

Here's the xaml for the MainWindow.
<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Width="1024" Height="768">
    <Grid Name="MainGrid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Name="TreeCol" Width="200"></ColumnDefinition>
            <ColumnDefinition Name="SplitterCol" Width="10"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" Background="Pink" Content="Tree"/>
        <GridSplitter Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Background="Gray" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" DragDelta="GridSplitter_DragDelta"/>
        <UserControl Name="UC" Grid.Row="0" Grid.Column="2" Grid.RowSpan="2"/>
    </Grid>
</Window>

The MainWindow code behind is responsible for tracking the movement of the grid splitter.

Class MainWindow
Public Sub New()
    InitializeComponent()
    UC.Content = New UserControl1
End Sub

Private Sub GridSplitter_DragDelta(sender As Object, e As Primitives.DragDeltaEventArgs)
    Dim d As Double = e.HorizontalChange
    ' The user control saved the button panel so we can get it here
    ' Each user control will save its button panel
    Dim ButtonLabel As FrameworkElement = Application.Current.Properties("ButtonPanel")
    Dim m As Thickness = ButtonLabel.Margin
    m.Left -= d
    ButtonLabel.Margin = m
End Sub
End Class

Now for the user control. We could have embedded it into MainWindow but my project requires the ability to dynamically load user controls. Fortunately this doesn't increase complexity very much. It only requires the user control to save a reference to its button panel in Application.Resources so that MainWindow can find it easily and adjust its left margin as the grid splitter moves. The User Control xaml looks like this.
<UserControl x:Class="UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.CommandBindings>
        <CommandBinding Command="Open" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"/>
    </UserControl.CommandBindings>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="100"></RowDefinition>
        </Grid.RowDefinitions>
        <Label Name="ClickPanel" Grid.Row="0" Content="Click [Open]"/>
        <Label Name="ButtonLabel" Grid.Row="1" Background="Beige" Margin="-210,0,0,0">
            <StackPanel Orientation="Horizontal">
                <Button Content="Open" Command="Open"/>
            </StackPanel>
        </Label>
    </Grid>
</UserControl>

Note how we are able to use regular command bindings - that was the whole point of this exercise. The initial left margin of the Button Label is -210 which is the initial width of the first and second columns of the MainWindow grid. Now the only thing the user control's code behind has to do is save a reference to its button panel to the Application Resources like this...

Public Class UserControl1

    Public Sub New()
        InitializeComponent()
        Application.Current.Properties("ButtonPanel") = ButtonLabel
    End Sub
    Private Sub CommandBinding_Executed(sender As Object, e As ExecutedRoutedEventArgs)
        ClickPanel.Content = "Well Done!"
    End Sub

    Private Sub CommandBinding_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs)
        e.CanExecute = True
        e.Handled = True
    End Sub
End Class

Note how the user control's code behind implements the command's CanExecute and Executed methods. Everything is in the user control, the CommandBinding, the buttons, and the CanExecute and Executed commands. This keeps everything simple(r).
The words "Click [Open]" and the [Open] button are part of the same user control. The Tree is in the parent window.

No comments:

Post a Comment