The State of Things

One of my pet hates with Microsoft’s WPF and Silverlight technologies, and one that no excuse can be found, is the total disregard that the developer shows for the user (designer, tester, translator) when developing a control. These WinForms + XAML developers are firmly seated back in the age where the design time was almost impossible to achieve. But, today this is no longer an excuse for providing substandard components.

So, what are the basic non-functional requirements that need to be covered when developing a custom control?

Well firstly, we need to consider our customers, the designers, testers and translators in this case, of our control. At the same time we need to consider the functionality we need to supply.

Let us take a simple example as a case study, a simple search control in an MVVM project. Firstly, I will make no reference to what flavour of MVVM is to be used; however the goal of our custom control is to be able to support MVVM via Commands rather than by Event Handlers. The search control is quite simple in its business requirements; it must inform the application when a search has to be executed as well as providing the search details, i.e. what to search for.

Already it should be clear the approach I am taking here. Firstly, define the business functionality of the control that is to be developed without reference to how it will finally look. Now let us consider then design requirements for the control. As we intend (hopefully) to reuse this control in other places in our project as well as in future development, we can only support a default styling with the ability to allow the designer to change or completely replace this default style. Hence another requirement: our control must be look-less in design.

Now, let us consider any other users that we might have, and consequently we have traditionally ignored. There are two that come to mind, firstly the tester who will want to test our control in isolation as well as in the use cases and business process where the control will be used. Secondly, we have our sales/documentation/translation user who wishes to use the application in which the control shall be used, in differing markets. Basically, this means taking care of the I18N and L10N concerns.

So what have we got now for requirements?

  1. The control shall supply searching functionality
    1. The control shall issue an indication that a search is to be executed.
      1. The control shall issue a command when the user selects the execute search.
    2. The control shall expose the search text.
    3. The control shall disable the executing of a new search until a current search has completed
    4. The control shall disable the entering of a new search text until a current search has completed
  2. The control shall be look-less
    1. A default skin will be provided.
    2. All visual aspects of the control shall be customisable in both VS2010 and Blend 4.
  3. The control shall be testable
    1. The control shall support UI Automation
    2. The control shall be able to participate in Coded UI testing.
  4. The control shall be support internationalisation and localisation
    1. The control shall support left to right and right to left languages.
    2. The control shall support translation of all text, widths, heights and fonts.
  5. The control shall support both the Visual Studio 2010 design time and the Blend 4 design time.

This covers the basic requirements for the moment. As we will see, the control will be enhanced later to supply functionality not considered in this basic requirements gathering exercise.

Now it is time to start our project. Open VS2010 and select a new WPF User Control Library project with the location of choice. Name this project SampleControls and rename the resulting UserControl.cs to SearchControl.cs.

Now is the time to consider our Business Requirements, i.e. the searching functionality. This consists of a small number of dependency properties, one for the search text, one for the disable functionality and another three for the command notification. The resultant code is as follows:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Markup;

namespace SampleControls
{
    /// <summary>
    /// Interaction logic for SearchControl.xaml
    /// </summary>
    [ContentProperty("SearchText")]
    [TemplatePart(Name = "PART_Search", Type = typeof(Button))]
    [TemplatePart(Name = "PART_SearchText", Type = typeof(TextBox))]
    public partial class SearchControl
    {
        /// <summary>
        /// Using a DependencyProperty as the backing store for SearchText.
        /// </summary>
        public static readonly DependencyProperty SearchTextProperty =
            DependencyProperty.Register("SearchText", typeof(string), typeof(SearchControl),
                                        new FrameworkPropertyMetadata(string.Empty));

        /// <summary>
        /// Using a DependencyProperty as the backing store for IsSearchEnabled.
        /// </summary>
        public static readonly DependencyProperty IsSearchEnabledProperty =
            DependencyProperty.Register("IsSearchEnabled", typeof(bool), typeof(SearchControl),
                                        new FrameworkPropertyMetadata(false));

        /// <summary>
        /// </summary>
        public static readonly RoutedEvent SearchTextChangedEvent =
            EventManager.RegisterRoutedEvent("SearchTextChanged",
                                             RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(SearchControl));

        /// <summary>
        /// Using a DependencyProperty as the backing store for SearchCommandTarget.
        /// </summary>
        public static readonly DependencyProperty SearchCommandTargetProperty =
            DependencyProperty.Register("SearchCommandTarget", typeof(IInputElement), typeof(SearchControl),
                                        new FrameworkPropertyMetadata(null));

        /// <summary>
        /// Using a DependencyProperty as the backing store for SearchCommandParameter.
        /// </summary>
        public static readonly DependencyProperty SearchCommandParameterProperty =
            DependencyProperty.Register("SearchCommandParameter", typeof(object), typeof(SearchControl),
                                        new FrameworkPropertyMetadata(null));

        /// <summary>
        /// Using a DependencyProperty as the backing store for SearchCommand.
        /// </summary>
        public static readonly DependencyProperty SearchCommandProperty =
            DependencyProperty.Register("SearchCommand", typeof(ICommand), typeof(SearchControl),
                                        new FrameworkPropertyMetadata(null));

        static SearchControl()
        {
            // Specify the gesture that triggers the command:
            var searchMouseGesture = new MouseGesture(MouseAction.LeftClick);
            CommandManager.RegisterClassInputBinding(typeof(SearchControl), new MouseBinding(srchCommand, searchMouseGesture));

            // Attach the command to custom logic:
            CommandManager.RegisterClassCommandBinding(typeof(SearchControl), new CommandBinding(srchCommand, AButtonClick));
        }

        /// <summary>
        /// As the button click.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.Input.ExecutedRoutedEventArgs"/> instance containing the event data.</param>
        private static void AButtonClick(object sender, ExecutedRoutedEventArgs e)
        {
            if (!(sender is SearchControl)) return;

            var sc = sender as SearchControl;
            sc.TheButtonClick(sender, e);
        }

        private static readonly RoutedUICommand srchCommand = new RoutedUICommand("Search", "SrchCommand", typeof(SearchControl));

        public static RoutedUICommand SrchCommand
        {
            get { return srchCommand; }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SearchControl"/> class.
        /// </summary>
        public SearchControl()
        {
            InitializeComponent();
            this.PART_SearchText.TextChanged += TheTextBoxTextChanged;
        }

        /// <summary>
        /// Gets or sets the search command target.
        /// </summary>
        /// <value>The search command target.</value>
        public IInputElement SearchCommandTarget
        {
            get { return (IInputElement)GetValue(SearchCommandTargetProperty); }
            set { SetValue(SearchCommandTargetProperty, value); }
        }

        /// <summary>
        /// Gets or sets the search command parameter.
        /// </summary>
        /// <value>The search command parameter.</value>
        public object SearchCommandParameter
        {
            get { return GetValue(SearchCommandParameterProperty); }
            set { SetValue(SearchCommandParameterProperty, value); }
        }

        /// <summary>
        /// Gets or sets the search command.
        /// </summary>
        /// <value>The search command.</value>
        public ICommand SearchCommand
        {
            get { return (ICommand)GetValue(SearchCommandProperty); }
            set { SetValue(SearchCommandProperty, value); }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is search enabled.
        /// </summary>
        /// <value>
        /// <c>True</c> if this instance is search enabled; otherwise, <c>false</c>.
        /// </value>
        public bool IsSearchEnabled
        {
            get { return (bool)GetValue(IsSearchEnabledProperty); }
            set { SetValue(IsSearchEnabledProperty, value); }
        }

        /// <summary>
        /// Gets or sets the search text.
        /// </summary>
        /// <value>The search text.</value>
        public string SearchText
        {
            get { return (string)GetValue(SearchTextProperty); }
            set { SetValue(SearchTextProperty, value); }
        }

        /// <summary>
        /// Executes the command source.
        /// </summary>
        /// <param name="command">The command.</param>
        /// <param name="parameter">The parameter.</param>
        /// <param name="target">The target.</param>
        private static void ExecuteCommandSource(ICommand command, object parameter, IInputElement target)
        {
            var command2 = command as RoutedCommand;

            if (command2 != null)
            {
                if (command2.CanExecute(parameter, target))
                {
                    command2.Execute(parameter, target);
                }
            }
            else if (command.CanExecute(parameter))
            {
                command.Execute(parameter);
            }
        }

        /// <summary>
        /// Handles the Click event of the theButton control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
        private void TheButtonClick(object sender, RoutedEventArgs e)
        {
            ExecuteCommandSource( this.SearchCommand, this.SearchCommandParameter, this.SearchCommandTarget );
        }

        /// <summary>
        /// Called when the <see cref="P:System.Windows.Controls.ContentControl.Content"/> property changes.
        /// </summary>
        /// <param name="oldContent">The old value of the <see cref="P:System.Windows.Controls.ContentControl.Content"/> property.</param>
        /// <param name="newContent">The new value of the <see cref="P:System.Windows.Controls.ContentControl.Content"/> property.</param>
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            if (oldContent != null)
                throw new InvalidOperationException("You can't change Content!");
        }

        /// <summary>
        /// Handles the TextChanged event of the theTextBox control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.Controls.TextChangedEventArgs"/> instance containing the event data.</param>
        private void TheTextBoxTextChanged(object sender, TextChangedEventArgs e)
        {
            SearchText = PART_SearchText.Text;
            e.Handled = true;
        }

        /// <summary>
        /// When overridden in a derived class, is invoked whenever application code or internal processes call <see cref="M:System.Windows.FrameworkElement.ApplyTemplate"/>.
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // Retrieve the Button from the current template
            var browseButton = GetTemplateChild("PART_Search") as Button;

            // Hook up the event handler
            if (browseButton != null)
                browseButton.Click += TheButtonClick;

            var searchText = GetTemplateChild("PART_SearchText") as TextBox;

            // hook up the evebt handler
            if (searchText != null)
                searchText.TextChanged += TheTextBoxTextChanged;
        }
    }
}

 

Now add a TextBox and a Button to the SearchControl.

The resulting XAML:

<UserControl x:Name="userControl" x:Class="SampleControls.SearchControl"
             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="40" d:DesignWidth="400" Height="Auto" Width="Auto">
    <Grid>
        <DockPanel>
            <Button x:Name="PART_Search" 
                Content="Search..." 
                DockPanel.Dock="Right" 
                Command="{Binding SrchCommand, ElementName=userControl, Mode=OneWay}" 
                IsEnabled="{Binding IsSearchEnabled, ElementName=userControl}"/>
            <TextBox x:Name="PART_SearchText" 
                MinWidth="{Binding ActualWidth, ElementName=PART_Search}" 
                Margin="0,0,5,0" 
                IsEnabled="{Binding IsSearchEnabled, ElementName=userControl}" 
            />
        </DockPanel>
    </Grid>
</UserControl>

The window hosting the Search control is quite simple as well:

        <SampleControls:SearchControlV3 x:Name="searchControl_Copy" 
            Width="Auto" 
            Grid.Row="3" 
            SearchCommand="{Binding TheSearchCommand, ElementName=window}" 
            SearchCommandParameter="{Binding RelativeSource={RelativeSource Self}}" 
            IsSearchEnabled="{Binding IsChecked, ElementName=toggleButton}" Margin="0" Grid.Column="1" 
            />

The basic control is now complete, or better said, basically complete. In the next step we shall be looking at making the control “look-less”.

 

Advertisements

~ by Intelligence4 on February 27, 2011.

 
%d bloggers like this: