Wednesday, 23 March 2011

Single-click editing for checkboxes in WPF and Silverlight datagrids

Possibly the single most annoying thing about the (otherwise excellent) datagrid in WPF and Silverlight is that the check box column requires two clicks to actually change state.  There’s a perfectly good reason for this – the cell must be selected before editing – but the user experience is a bit irritating.  I first came across this problem in WPF and found a solution, only to discover that the same thing wouldn’t work in Silverlight.  So this post is more of an aide memoire to myself for the next time this happens.

I should add that this isn’t completely original code, more of a piecing together of code from much cleverer people on the Silverlight forums and Stack Overflow, but it works happily with MVVM/data binding.  Both versions use the FindVisualChildren method mentioned in this post:

public static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj)
    where T : DependencyObject
{
    if (depObj != null)
    {
        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
            if (child != null && child is T)
            {
                yield return (T)child;
            }

            foreach (T childOfChild in FindVisualChildren<T>(child))
            {
                yield return childOfChild;
            }
        }
    }
}

The WPF version:

public static class DataGridCellHelper
{
    #region IsSingleClickInCell

    public static readonly DependencyProperty IsSingleClickInCellProperty =
        DependencyProperty.RegisterAttached("IsSingleClickInCell",
        typeof(bool), typeof(DataGrid),
        new FrameworkPropertyMetadata(false, OnIsSingleClickInCellSet));

    public static void SetIsSingleClickInCell(UIElement element, bool value)
    {
        element.SetValue(IsSingleClickInCellProperty, value);
    }

    public static bool GetIsSingleClickInCell(UIElement element)
    {
        return (bool)element.GetValue(IsSingleClickInCellProperty);
    }

    private static void OnIsSingleClickInCellSet(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        if (!(bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
        {
            if ((bool)e.NewValue)
            {
                var dataGrid = sender as DataGrid;
                Debug.Assert(dataGrid != null);

                EventManager.RegisterClassHandler(typeof(DataGridCell),
                    DataGridCell.PreviewMouseLeftButtonUpEvent,
                    new RoutedEventHandler(OnPreviewMouseLeftButtonDown));
            }
        }
    }

    private static void OnPreviewMouseLeftButtonDown(object sender, RoutedEventArgs e)
    {
        DataGridCell cell = sender as DataGridCell;

        if (cell != null && !cell.IsEditing && !cell.IsReadOnly)
        {
            var checkBoxes = ControlHelper.FindVisualChildren<CheckBox>(cell);

            if (checkBoxes != null && checkBoxes.Count() > 0)
            {
                foreach (var checkBox in checkBoxes)
                {
                    if (checkBox.IsEnabled)
                    {
                        checkBox.Focus();
                        checkBox.IsChecked = !checkBox.IsChecked;

                        var bindingExpression = checkBox.GetBindingExpression(CheckBox.IsCheckedProperty);

                        if (bindingExpression != null)
                        {
                            bindingExpression.UpdateSource();
                        }
                    }
                    break;
                }
            }
        }
    }

    #endregion
}

The Silverlight version:

public static class DataGridCellHelper
{
    #region IsSingleClickInCell

    public static readonly DependencyProperty IsSingleClickInCellProperty =
        DependencyProperty.RegisterAttached("IsSingleClickInCell",
        typeof(bool), typeof(DataGrid),
        new PropertyMetadata(false, OnIsSingleClickInCellSet));

    public static void SetIsSingleClickInCell(UIElement element, bool value)
    {
        element.SetValue(IsSingleClickInCellProperty, value);
    }

    public static bool GetIsSingleClickInCell(UIElement element)
    {
        return (bool)element.GetValue(IsSingleClickInCellProperty);
    }

    public static void OnIsSingleClickInCellSet(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        if (!(bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(
            typeof(DependencyObject)).DefaultValue))
        {
            if ((bool)e.NewValue)
            {
                var dataGrid = sender as DataGrid;
                Debug.Assert(dataGrid != null);

                dataGrid.MouseLeftButtonUp += OnDataGridMouseLeftButtonUp;
            }
        }
    }

    private static void OnDataGridMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        var dataGrid = sender as DataGrid;
        Debug.Assert(dataGrid != null);

        var hitPoint = e.GetPosition(null);
        var controlsInRange = VisualTreeHelper.FindElementsInHostCoordinates(
                                    hitPoint, dataGrid);
        var dataGridCellInRange = (from element in controlsInRange
                                    where element is DataGridCell
                                    select element).FirstOrDefault();

        if (dataGridCellInRange != null)
        {
            HandleSingleClickCheckBox(dataGridCellInRange);
        }
    }

    private static void HandleSingleClickCheckBox(UIElement dataGridCellInRange)
    {
        var checkBox = ControlHelper.FindVisualChildren<CheckBox>(
            dataGridCellInRange).FirstOrDefault();

        if (checkBox != null)
        {
            if (checkBox.IsEnabled)
            {
                checkBox.Focus();
                checkBox.IsChecked = !checkBox.IsChecked;

                var bindingExpression = checkBox.GetBindingExpression(CheckBox.IsCheckedProperty);

                if (bindingExpression != null)
                {
                    bindingExpression.UpdateSource();
                }
            }
        }
    }

    #endregion
}

Usage is the same in both (where p is the reference to the namespace that the DataGridCellHelper class is in):

<DataGrid p:DataGridCellHelper.IsSingleClickInCell="True" />

This should ensure that any checkbox columns respond to a single click, even if data bound.

Tuesday, 8 March 2011

Custom role authorisation with Windows authentication in WCF

I’ve just published my first project on CodePlex, which is a WPF/MVVM tool based on the IDesign credentials manager, so it seemed an appropriate time to blog about using ASP.NET providers for authorisation while using Windows for authentication with WCF services.  Admittedly this isn’t an original idea, in fact ScottGu blogged about it back in 2006, but for ASP.NET web applications instead.  This is simply a variation on that.

Aside from the fact that SharePoint has done this since WSS 3.0/MOSS 2007, why would you want to do this?   the main reason is because you may not have control over Active Directory (if you don’t regularly see any of the people who run the servers that you work on, then this is probably true of your situation) but you want to provide granular permissions on an application or service.  Eventually you’ll want to delegate role management to an end-user so the product owners can be responsible for their own security.  In my experience, the larger a company gets, the longer it takes to get a system access change request done – so this type of requirement is quite common.  There are technical and business advantages to using Windows authentication (instead of the ASP.NET membership provider); such as being able to rely on the ambient security of your business domain for user authentication, but also that it removes the need for certificates in development or production.  Often, network security administrators don’t like to create AD groups to meet the requirements of specific applications, so this is a “best of both worlds” solution that should keep everyone happy.

Implementation is done in the web.config.  Basically it’s no different from using ASP.NET providers for everything (authentication and authorisation) except that you can miss out the authentication bits as all the main bindings default to Windows authentication.  The only caveat is for the basicHttpBinding (which is insecure by default) where the security and transport client credentials have to be explicitly defined to use Windows authentication.  The important bits are below:

<?xml version="1.0"?>
<configuration>
  <connectionStrings>
    <!--
      This is the connection string to the aspnetdb/credentials store database
      Update as appropriate
    -->
    <add name="AspNetDb" connectionString="Data Source=MyServerName;Initial Catalog=aspnetdb;Integrated Security=True;enlist=false" providerName="System.Data.SqlClient"/>
  </connectionStrings>
  <system.web>
    <!--
      Replace the application name with the name of your application in the aspnetdb/credentials store database
    -->
        <roleManager enabled="true" defaultProvider="SqlRoleProvider">
          <providers>
            <add
              name="SqlRoleProvider"
              type="System.Web.Security.SqlRoleProvider"
              connectionStringName="AspNetDb"
              applicationName="MyApplication"
                      />
          </providers>
        </roleManager>
    </system.web>
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding>
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
        </binding>
      </basicHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceAuthorization
                        principalPermissionMode="UseAspNetRoles"
                        roleProviderName="SqlRoleProvider" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

The client-side config for basicHttpBinding is:

<system.serviceModel>
<bindings>
  <basicHttpBinding>
    <binding>
      <security mode="TransportCredentialOnly">
        <transport clientCredentialType="Windows" proxyCredentialType="None"
            realm="" />
      </security>
    </binding>
  </basicHttpBinding>
</bindings>
</system.serviceModel>