Monday, 30 April 2007

“Default” buttons in WPF and multiple default buttons per page

Most user interface frameworks have the concept of a default button. The default button is the button which is activated when the Enter key is pressed. This is an important aspect for usability. For example, on a search form, most users expect to be able to perform a search by pressing Enter on any form element. If this does not happen as expected, the user gets frustrated as they need to either tab onto the button and press Enter, or use the mouse to click. I think the web has pushed this standard as HTML forms automatically post themselves when the user presses Enter within them. WPF offers the ability to have both Default and Cancel buttons. The Cancel button is activated by pressing the ESC key, although in my opinion this is not quite as common as the Enter button as most users don’t expect to be able to press ESC within forms – only dialog windows maybe. How do default buttons work? There is nothing particularly special going on under the covers when you set a button to be the default button. When the IsDefault property is set, AccessKeyManager.Register is called, passing in “\r” as the access key. Similarly, the act of setting IsCancel calls AccessKeyManager.Register passing in the character code for ESC - “\x001b”. The only special thing that setting IsDefault will do is ensure that the IsDefaulted property is updated correctly. This allows, for example, buttons to be styled differently if they are defaulted. How do I get multiple default buttons per page? In ASP.NET a common problem was that because you are forced to have a single server-side form, it was not possible to have multiple logical forms on the page and have the Enter button behave as expected without a lot of hacking. For example, you might want the template for all your pages to have a small search form at the top with a search button, and also have the search performed when the user presses enter within the search box. When the user is on a page with its own form on it, you would want that form to be submitted when the user pressed enter. This was not possible natively. This issue was solved in ASP.NET2 by allowing any Panel or Table to have set a DefaultButton property. Behind the scenes this would set up an event handler in JavaScript which would activate the button when enter was pressed. In WPF, there is no such property. If you define two panels on a page, and into each panel place a TextBox and a Button with IsDefault="True", you will see that pressing enter in either TextBox always activates the first button. This is because that button registered itself first so is at the top of the invocation list of the Enter access key:

<Window x:Class="WindowsApplication7.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WindowsApplication7"
    Title="WindowsApplication7" Height="300" Width="300"
    >
    <StackPanel>
        <StackPanel Margin="5" Background="Yellow">
            <TextBox Margin="5" />
            <Button Margin="5" IsDefault="True" Content="Yellow" />
        </StackPanel>
        <StackPanel Margin="5" Background="Green">
            <TextBox Margin="5" />
            <Button Margin="5" IsDefault="True" Content="Green" />
        </StackPanel>
    </StackPanel>
</Window>
In the above example, no matter which TextBox has focus, the Yellow button is always activated when you press enter. To solve this problem, there is a fairly undocumented feature within the AccessKeyManager called scoping. This allows you to define a scope for an access key such that it is only activated within that scope. The way this is done is by handling the routed event and adding the scope to the event args. Then when it returns to the AccessKeyManager, it detects that it has been scoped and will only apply it to buttons within that scope. The code is quite simple to write, but I have created a small helper class to encourage readibility and code reuse. In WPF it’s so easy to write small helper classes and seamlessly integrate via the use of attached properties, so I made an attached property called AccessKeyScoper.IsAccessKeyScope which can be applied to any element. With this, the above example simply needs to be modified as follows, and the correct button will be activated:
<StackPanel Margin="5" Background="Yellow" local:AccessKeyScoper.IsAccessKeyScope="True">
    <TextBox Margin="5" />
    <Button Margin="5" IsDefault="True" Content="Yellow" />
</StackPanel>
<StackPanel Margin="5" Background="Green" local:AccessKeyScoper.IsAccessKeyScope="True">
    <TextBox Margin="5" />
    <Button Margin="5" IsDefault="True" Content="Green" />
</StackPanel>
The code for the helper class is here:
using System;
using System.Windows;
using System.Windows.Input;
namespace WindowsApplication7
{
    /// <summary>
    ///    Class used to manage generic scoping of access keys
    /// </summary>
    public static class AccessKeyScoper
    {
        /// <summary>
        ///    Identifies the IsAccessKeyScope attached dependency property
        /// </summary>
        public static readonly DependencyProperty IsAccessKeyScopeProperty =
            DependencyProperty.RegisterAttached("IsAccessKeyScope", typeof(bool), typeof(AccessKeyScoper), new PropertyMetadata(false, HandleIsAccessKeyScopePropertyChanged));
        /// <summary>
        ///    Sets the IsAccessKeyScope attached property value for the specified object
        /// </summary>
        /// <param name="obj">The object to retrieve the value for</param>
        /// <param name="value">Whether the object is an access key scope</param>
        public static void SetIsAccessKeyScope(DependencyObject obj, bool value)
        {
            obj.SetValue(AccessKeyScoper.IsAccessKeyScopeProperty, value);
        }
        /// <summary>
        ///    Gets the value of the IsAccessKeyScope attached property for the specified object
        /// </summary>
        /// <param name="obj">The object to retrieve the value for</param>
        /// <returns>The value of IsAccessKeyScope attached property for the specified object</returns>
        public static bool GetIsAccessKeyScope(DependencyObject obj)
        {
            return (bool) obj.GetValue(AccessKeyScoper.IsAccessKeyScopeProperty);
        }
        private static void HandleIsAccessKeyScopePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue.Equals(true))
            {
                AccessKeyManager.AddAccessKeyPressedHandler(d, HandleScopedElementAccessKeyPressed);
            }
            else
            {
                AccessKeyManager.RemoveAccessKeyPressedHandler(d, HandleScopedElementAccessKeyPressed);
            }
        }
        private static void HandleScopedElementAccessKeyPressed(object sender, AccessKeyPressedEventArgs e)
        {
            if (!Keyboard.IsKeyDown(Key.LeftAlt) && !Keyboard.IsKeyDown(Key.RightAlt) && GetIsAccessKeyScope((DependencyObject)sender))
            {
                e.Scope = sender;
                e.Handled = true;
            }
        }
    }
}

Saturday, 28 April 2007

Pasting to multiple cells in Xceed WPF DataGrid

In my previous article, Implementing Copy & Paste between cells in Xceed WPF DataGrid, I outined the method I was using to copy and paste values between cells in the Xceed WPF grid. To recap, this involves using the ApplicationCommands to register to the Copy command and copying the content to the clipboard as text. We then register to the Paste command and when this is received, set the current cell to Edit mode, simulate the text input, and commit the change. The next requirement is to support the selection of a range of cells, and to allow the user to paste a single value to all selected cells. Selection should be in one of three ways - either by dragging the mouse from the first cell to the last, by clicking the cell then clicking the last with shift held down, or by clicking cells with control pressed. As Xceed grid doesn't support multiple cell selection, and I don't want to mess around with the internal workings of Xceed's grid, I decided to create an attached property of type Boolean to attach to each selected cell. The attached property would indicate that that cell is currently selected. However, I then realised that as the Xceed grid is not inheriting from the Selector class, I might as well just reuse the Selector.IsSelected attached property, and avoid having to create my own. So here is the function to select a range of cells and set the attached property on them all. The range to be selected is defined by the "current" cell (which we can get natively from the DataGridControl) and a cell passed to the function.

        /// <summary>
        ///     Selects the range of cell from the "current" cell to the specified cell
        /// </summary>
        /// <param name="toCell">The cell to select to</param>
        private static void SelectRangeFromCurrent(Cell toCell)
        {
            DataGridControl dataGridControl = DataGridControl.GetParentDataGridControl(toCell);
 
            // Need a current item to do anything
            if (dataGridControl.CurrentItem == null)
            {
                return;
            }
 
            // Find the current cell
            Row currentRow = (Row) dataGridControl.GetContainerFromItem(dataGridControl.CurrentItem);
            Cell currentCell = null;
 
            if (currentRow != null && dataGridControl.CurrentColumn != null)
            {
                currentCell = currentRow.Cells[dataGridControl.CurrentColumn.Index];
            }
 
            // Do nothing if it's being edited
            if (currentCell == null || currentCell.IsBeingEdited)
            {
                return;
            }
 
            // Clear the selected status on all the cells
            ClearSelectedStatus(DataGridControl.GetParentDataGridControl(toCell));
 
            // Get the indexes of the rows and columns we need to loop through
            int fromRow, toRow, fromColumn, toColumn;
            GetFromAndToRowAndColumns(dataGridControl, currentCell, toCell, out fromRow, out toRow, out fromColumn, out toColumn);
 
            // Loop and set the IsSelected on all cells
            for (int rowIndex = fromRow; rowIndex <= toRow; rowIndex++)
            {
                Row row = (Row) dataGridControl.GetContainerFromIndex(rowIndex);
                for (int columnIndex = fromColumn; columnIndex <= toColumn; columnIndex++)
                {
                    Selector.SetIsSelected(row.Cells[columnIndex], true);
                }
            }
This function uses two other functions - ClearSelectedStatus(), which clears the IsSelected on all cells in a grid, and GetFromAndToRowAndColumns() which works out the indexes of the start and end points based on the cells. Once we have the indexes, it's a simple case of looping through setting the IsSelected on all cells between those indexes. Visually, setting the attached property will do nothing to the cells (and rightly so - this WPF we're talking about!). Therefore I define a style in my App.xaml which will change the background colour of the cells when the IsSelected is set
    <Style TargetType="xcdg:DataCell">
        <Style.Triggers>
            <Trigger Property="Selector.IsSelected" Value="True">
                <Setter Property="Background" Value="{StaticResource LightBlueBrush}" />
            </Trigger>
        </Style.Triggers>
    </Style>
So now that we can select multiple of cells, we need to wire up the event handling to actually trigger the selection. We subscribe to the MouseDown and MouseEnter events and detect if the left mouse button is pressed. If so, we act accordingly:
        /// <summary>
        ///     Handles when the mouse goes down over a cell, either deselecting everything, or selecting the single cell (if control is pressed)
        ///     or selecting the range of cells (if shift is pressed)
        /// </summary>
        private static void HandleCellMouseDown(object sender, RoutedEventArgs e)
        {
            Cell cell = sender as Cell;
 
            if (cell != null && Mouse.LeftButton == MouseButtonState.Pressed)
            {
                if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
                {
                    SelectRangeFromCurrent(cell);
                }
                else if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
                {
                    Selector.SetIsSelected(cell, true);
                }
                else
                {
                    ClearSelectedStatus(DataGridControl.GetParentDataGridControl(cell));
                }
            }
        }
 
        /// <summary>
        ///     Handles when the mouse enters a cell with the left button depressed, selecting the appropriate range of cells
        /// </summary>
        private static void HandleCellMouseEnter(object sender, RoutedEventArgs e)
        {
            Xceed.Wpf.DataGrid.Cell cell = sender as Xceed.Wpf.DataGrid.Cell;
 
            if (cell != null && Mouse.LeftButton == MouseButtonState.Pressed)
            {
                SelectRangeFromCurrent(cell);
            }
        }
And that's it - we have multiple cell selection working pretty nicely! The last thing to do is update my previous code for pasting, and rather than just pasting to the current cell we paste to every cell which has the Selector.IsSelected property set to true. I have put all the code to do this into a helper class - DataGridControlHelper.cs. Therefore I won't include any more code here as it's mostly straightforward glue code with a few usability tweaks (such as unselecting everything when someone navigates with the keyboard). Just call the static RegisterEventHandlers() method when your application starts up.

Wednesday, 25 April 2007

Implementing Copy & Paste between cells in Xceed WPF DataGrid

I am using the Xceed grid in my application to allow the user to enter monetary values into cells. They would like to be able to copy and paste values between cells in the grid, but this is not supported by the grid natively. Support from the Clipboard is very good in WPF (via the System.Windows.Clipboard static class) so this actually is not a particularly difficult thing to implement. My aim is to write a generic reusable solution here that will work for any Xceed grid in my application, rather than a specific one, so I neither want to do any copying or updating of actual data. What we first need to do is register execution handlers for the ApplicationCommands.Copy and the ApplicationCommands.Paste commands against the DataGrid. We then either copy the content of the current cell or paste into it depending on which command is executed. I think the best place to perform generic event handling against a class is in the static initialiser of the App class, like follows:

        static App()
        {
            CommandManager.RegisterClassCommandBinding(typeof(DataGridControl), newCommandBinding(ApplicationCommands.Paste, HandleXceedPasteCommandExecuted)); 
            CommandManager.RegisterClassCommandBinding(typeof(DataGridControl), newCommandBinding(ApplicationCommands.Copy, HandleXceedCopyCommandExecuted)); 
        }
We'll start with the Copy handler. This is quite simple - we first work out which cell has the focus, then copy the textual version of its content to the clipboard.
        private static void HandleXceedCopyCommandExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            DataGridControl dataGridControl = sender as DataGridControl;
 
            if (dataGridControl != null)
            {
                Xceed.Wpf.DataGrid.Row row = (Xceed.Wpf.DataGrid.Row) dataGridControl.ItemContainerGenerator.ContainerFromItem(dataGridControl.CurrentItem);
                Xceed.Wpf.DataGrid.Cell currentCell = row.Cells[dataGridControl.CurrentColumn.Index];
 
                if (currentCell != null && currentCell.HasContent)
                {
                    Clipboard.SetText(currentCell.Content.ToString());
                }
            }
        }
Note that we copy the ToString() of the content to the clipboard, so this technique would not work if the Cell's content was an object whose string was not valid to be entered into a Cell. As we are copying and pasting between Cells, this shouldn't be a problem. Now onto the paste command. The paste command is more tricky and I will explain why. We could just update the content directly, but that would bypass any text input handling and formatting that we may be doing in the Xceed grid. For example, one grid in my application uses the NumericInputScope class to block all non numbers from being typed into the Grid. If we just updated the grid at this point, the PreviewKeyDown and PreviewTextInput would never be fired, therefore we would be allowing invalid characters to be typed into the grid. The solution I came up with for this is to switch the cell into Edit Mode, simulate keypresses for each character being pasted (via raising the TextInput event), and the commit the changes back to the cell. This should ensure that the paste is essentially doing exactly the same thing as it would do if the user pressed the keys themselves! Note we dispatch the work back to the Dispatcher... this is to allow the cell's editor to activate and the cell to get focus before we simulate the key presses.
        public delegate void Callback();
 
        private static void HandleXceedPasteCommandExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            DataGridControl dataGridControl = sender as DataGridControl;
 
            if (dataGridControl != null && Clipboard.ContainsText())
            {
                Xceed.Wpf.DataGrid.Row row = (Xceed.Wpf.DataGrid.Row)dataGridControl.ItemContainerGenerator.ContainerFromItem(dataGridControl.CurrentItem);
                Xceed.Wpf.DataGrid.Cell currentCell = row.Cells[dataGridControl.CurrentColumn.Index];
 
                if (!currentCell.ReadOnly)
                {
                    string text = Clipboard.GetText();
 
                    currentCell.BeginEdit();
 
                    Application.Current.Dispatcher.Invoke(DispatcherPriority.Input, (Callback)delegate
                    {
                        if (Keyboard.FocusedElement != null)
                        {
                            foreach (char c in text)
                            {
                                TextComposition textComposition = new TextComposition(InputManager.Current, Keyboard.FocusedElement, new string(c, 1));
 
                                TextCompositionEventArgs textCompositionEventArgs = new TextCompositionEventArgs(Keyboard.PrimaryDevice, textComposition);
                                textCompositionEventArgs.RoutedEvent = UIElement.TextInputEvent;
 
                                Keyboard.FocusedElement.RaiseEvent(textCompositionEventArgs);
                            }
                        }
 
                        currentCell.EndEdit();
                    });
                }
            }
        }
I'd be interested to know what people think of this method, and if anyone has tried any other solutions to implement copy and paste in the grid let me know. The next requirement is to support selection of and pasting to multiple cells within the Grid. Again, this is not currently supported in the Xceed DataGrid, but I will hopefully come up with a way of doing it!

Monday, 23 April 2007

XAML Support in Resharper 3.0

I am an avid Jetbrains Resharper user and have been for years, I really think it increases the usability of Visual Studio above and beyond that provided by Microsoft. I regularly return to their site to check on the new releases and see what they're working on, and just noticed that they have started adding some support for XAML to their latest EAP Release. So having installed it and opened my solution, I open a XAML file to see what has happened. You can immediately see that Resharper is having an effect because things show up in completely different colours - there seems to be some processing happening on every tag in the XAML file (this processing all happens in the background so doesn't slow things down very much). So, now type names show up in blue and property names show up in purple. This is consistent with the rest of Resharpers colourings, although these are of course configurable. When you have the keyboard over a tag, there are now two context sensitive options, these are "Replace all tags" and "Replace all attributes". I think this just comes from the new changes they have made for XML refactoring and aren't hugely useful for XAML. It might be useful if you want to replace all TextBoxes with say, Labels or something, or if you rename a property on a type and want all references to be renamed (although I imagine this will eventually happen automatically). The only truly useful feature I can find simply comes from the fact that references are being resolved, so the existing Resharper feature of "Go To Definition (Ctrl+B) can be used on them. So you can now press Ctrl+B on any property or object name and get instantly taken to the definition of that. This is great for Styles and other resources (e.g. where you have Style="{StaticResource MyStyle}" you can navigate straight to the style) and using it on anything else seems to take you to the object browser for that type or property (previously you would just get taken to relevant line in the XmlPresentation.xsd file). That's about it really - overall I can't see much extra that this is adding to the XAML user experience, but I can see, by programatically resolving references that they are laying the groundworks for some very useful features. However there is still a lot to do on this subject - for example, open any complex XAML file and the first thing you will notice if you are using any custom namespaces or custom controls, is that Resharper is not picking them up. They do state on their site that they are working on improving this.

Saturday, 21 April 2007

New tool for viewing WPF default control templates

I have always used StyleSnooper to get at the templates for my WPF controls. It's pretty useful and simple, and can get you the style and templates.

Now I just stumbled into a new tool, by Chris Sells and Ian Griffiths, called ShowMeTheTemplate.

This is nice. It formats the output a lot better than StyleSnooper (you can expand and collapse ala IE) and it splits the different templates (e.g HeaderTemplate and ContentTemplate into separate text boxes). You also get to see a preview of the control you are viewing the template for. Finally, you get to choose which Theme to use.

This should prove a useful addition to the WPF developer's toolkit especially combined with StyleSnooper for seeing all the other styles applied by default to a control.

Friday, 20 April 2007

Good font for use in Visual Studio

I always used Courier New in Visual Studio. Now it seems Microsoft have come out with a new font called Consolas which was specifically designed for use in programming environments. I have been using it for the last few months and definitely prefer it to Courier New. It actually comes pre-installed on Vista, but if you don't have it, you can get it here.