Thursday, August 21, 2014

WPF TreeView with Multiple Selection

With the WPF TreeView it is not possible to select multiple items. But it can be easiliy extended. Therefore an Attached Property can be used. First we are adding the used namespace to the control.
xmlns:e="clr-namespace:MultipleSelectionTreeView;
                                            assembly=MultipleSelectionTreeView"
Then we can add the Attached Property to the TreeView. With IsMultipleSelection="True" we activate the multiple selection in the TreeView. With the property SelectedItems we can bind the multiple selected items to a property of the DataContext. We can define the style of a multiple selected TreeViewItem within a Style Trigger. Therefore the property IsItemSelected is used.
<TreeView ItemsSource="{Binding Elements}"
          MinHeight="20"
          e:TreeViewMultipleSelectionAttached.IsMultipleSelection="True"
          e:TreeViewMultipleSelectionAttached.SelectedItems=
                                                   "{Binding SelectedElements}">
    <TreeView.Resources>
        <Style x:Key="{x:Type TreeViewItem}" BasedOn="{StaticResource 
                                                        {x:Type TreeViewItem}}" 
                                                        TargetType=
                                                        "{x:Type TreeViewItem}">
            <Style.Triggers>
                <Trigger 
                   Property="e:TreeViewMultipleSelectionAttached.IsItemSelected"
                         Value="True">
                    <Setter Property="Background"
                            Value="LightGreen" />
                </Trigger>
            </Style.Triggers>
        </Style>
        <HierarchicalDataTemplate DataType="{x:Type e:Element}"
                                  ItemsSource="{Binding ChildElements}">
            <StackPanel Orientation="Horizontal"
                        VerticalAlignment="Stretch"
                        Margin="0,2,0,2">
                <TextBlock Text="{Binding ElementName}"
                           Margin="5,0,0,0"
                           VerticalAlignment="Center" />
            </StackPanel>
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate DataType="{x:Type e:ChildElement}">
            <StackPanel Orientation="Horizontal"
                        VerticalAlignment="Stretch"
                        Margin="0,2,0,2">
                <TextBlock Text="{Binding ElementName}"
                           VerticalAlignment="Center"
                           Margin="5,0,0,0" />
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.Resources>
</TreeView>
Now have a look at the implementation of the Attached Property. To create a custom Attached Property a static DependencyProperty is defined by using the RegisterAttached method. This method has the possibility to set a PropertyMetadata as parameter in that a PropertyChangedCallback can be defined by using Delegates. In the callback method an event handler is added or removed to DependencyObject. First we define the IsMultipleSelection property. With that a TreeView is marked in XAML to have multiple selection abilities.
    public static readonly DependencyProperty IsMultipleSelectionProperty =
        DependencyProperty.RegisterAttached(
            "IsMultipleSelection",
            typeof(Boolean),
            typeof(TreeViewMultipleSelectionAttached),
            new PropertyMetadata(false, OnMultipleSelectionPropertyChanged));
 
    public static bool GetIsMultipleSelection(TreeView element)
    {
        return (bool)element.GetValue(IsMultipleSelectionProperty);
    }
 
    public static void SetIsMultipleSelection(TreeView element, Boolean value)
    {
        element.SetValue(IsMultipleSelectionProperty, value);
    }
In the RegisterAttached method we have defined a PropertyMetadata object that defines a call back method (OnMultipleSelectionPropertyChanged) that is called if the property has changed. In that method a handler for the MouseLeftButtonDownEvent of TreeViewItem is added or removed to the TreeView. Remember to mark the handledEventsToo parameter in the AddHandler method as true to allow calling the handler OnTreeViewItemClicked correctly.
    private static void OnMultipleSelectionPropertyChanged(DependencyObject d,
                                         DependencyPropertyChangedEventArgs e)
    {
        TreeView treeView = d as TreeView;
 
        if (treeView != null)
        {
            if (e.NewValue is bool)
            {
                if ((bool)e.NewValue)
                {
                    treeView.AddHandler(TreeViewItem.MouseLeftButtonDownEvent,
                      new MouseButtonEventHandler(OnTreeViewItemClicked), true);
                }
                else
                {
                   treeView.RemoveHandler(TreeViewItem.MouseLeftButtonDownEvent,
                        new MouseButtonEventHandler(OnTreeViewItemClicked));
                }
            }
        }
    }
The OnTreeViewItemClicked method is now called on each click at a TreeViewItem if IsMultipleSelection for a TreeView is set to true.
    private static void OnTreeViewItemClicked(object sender, MouseButtonEventArgs e)
    {
        TreeViewItem treeViewItem = FindTreeViewItem(
                                        e.OriginalSource as DependencyObject);
        TreeView treeView = sender as TreeView;
 
        if (treeViewItem != null && treeView != null)
        {
            if (Keyboard.Modifiers == ModifierKeys.Control)
            {
                SelectMultipleItemsRandomly(treeView, treeViewItem);
            }
            else if (Keyboard.Modifiers == ModifierKeys.Shift)
            {
                SelectMultipleItemsContinuously(treeView, treeViewItem);
            }
            else
            {
                SelectSingleItem(treeView, treeViewItem);
            }
        }
    }
First we need to find the TreeViewItem that is invoked with the click. This happens with the recursive FindTreeViewItem method.
    private static TreeViewItem FindTreeViewItem(DependencyObject dependencyObject)
    {
        if (dependencyObject == null)
        {
            return null;
        }
 
        TreeViewItem treeViewItem = dependencyObject as TreeViewItem;
        if (treeViewItem != null)
        {
            return treeViewItem;
        }
 
        return FindTreeViewItem(VisualTreeHelper.GetParent(dependencyObject));
    }
Then the OnTreeViewItemClicked method checks what kind of click is happening. There are Left-Mouse-Button (LMB) + Ctrl, LMB + Shift, and all other cases with LMB. In the third case a single click is performed.
    private static void SelectSingleItem(TreeView treeView,
                                                    TreeViewItem treeViewItem)
    {
        // first deselect all items
        DeSelectAllItems(treeView, null);
        SetIsItemSelected(treeViewItem, true);
        SetStartItem(treeView, treeViewItem);
    }
Therefore at first all items are deselected by calling the recursive method DeSelectAllItems. 
    private static void DeSelectAllItems(TreeView treeView,
                                                 TreeViewItem treeViewItem)
    {
        if (treeView != null)
        {
            for (int i = 0; i < treeView.Items.Count; i++)
            {
                TreeViewItem item = treeView.ItemContainerGenerator.
                                           ContainerFromIndex(i) as TreeViewItem;
                if (item != null)
                {
                    SetIsItemSelected(item, false);
                    DeSelectAllItems(null, item);
                }
            } 
        }
        else
        {
            for (int i = 0; i < treeViewItem.Items.Count; i++)
            {
                TreeViewItem item = treeViewItem.ItemContainerGenerator.
                                           ContainerFromIndex(i) as TreeViewItem;
                if (item != null)
                {
                    SetIsItemSelected(item, false);
                    DeSelectAllItems(null, item);
                }
            } 
        }
    }
Selection and deselection is happening by the DependencyProperty IsItemSelected that we have also defined. This poperty is used in XAML to apply a certain style on the selected TreeViewItems.
    public static readonly DependencyProperty IsItemSelectedProperty =
        DependencyProperty.RegisterAttached(
            "IsItemSelected",
            typeof(Boolean),
            typeof(TreeViewMultipleSelectionAttached),
            new PropertyMetadata(false, OnIsItemSelectedPropertyChanged));
 
    public static bool GetIsItemSelected(TreeViewItem element)
    {
        return (bool)element.GetValue(IsItemSelectedProperty);
    }
 
    public static void SetIsItemSelected(TreeViewItem element, Boolean value)
    {
        element.SetValue(IsItemSelectedProperty, value);
    }
In the RegisterAttached method we have defined a PropertyMetadata object that defines a call back method (OnIsItemSelectedPropertyChanged) that is called if the property (selection of the TreeViewItem) has changed. In that method the Header of TreeViewItem will be added to or removed to a List of all selected TreeViewItems. This List is associated with the corresponding TreeView.
    private static void OnIsItemSelectedPropertyChanged(DependencyObject d,
                                           DependencyPropertyChangedEventArgs e)
    {
        TreeViewItem treeViewItem = d as TreeViewItem;
        TreeView treeView = FindTreeView(treeViewItem);
        if (treeViewItem != null && treeView != null)
        {
            var selectedItems = GetSelectedItems(treeView);
            if (selectedItems != null)
            {
                if (GetIsItemSelected(treeViewItem))
                {
                    selectedItems.Add(treeViewItem.Header);
                }
                else
                {
                    selectedItems.Remove(treeViewItem.Header);
                }
            }
        }
    }
To find the TreeView corresponding to the TreeViewItem the recursive method FindTreeView is used.
    private static TreeView FindTreeView(DependencyObject dependencyObject)
    {
        if (dependencyObject == null)
        {
            return null;
        }
 
        TreeView treeView = dependencyObject as TreeView;
        if (treeView != null)
        {
            return treeView;
        }
 
        return FindTreeView(VisualTreeHelper.GetParent(dependencyObject));
    }
To associate the List of all selected TreeViewItems with the corresponding TreeView the DependencyProperty SelectedItems is used. This property can be used in XAML to bind the multiple selected items to a property of the DataContext.
    public static readonly DependencyProperty SelectedItemsProperty =
        DependencyProperty.RegisterAttached(
            "SelectedItems",
            typeof(IList),
            typeof(TreeViewMultipleSelectionAttached),
            new PropertyMetadata());
 
    public static IList GetSelectedItems(TreeView element)
    {
        return (IList)element.GetValue(SelectedItemsProperty);
    }
 
    public static void SetSelectedItems(TreeView element, IList value)
    {
        element.SetValue(SelectedItemsProperty, value);
    }
Now let's go back to the SelectSingleItem method. After deselecting all TreeViewItems by setting the IsItemSelected DependencyProperty to false, the clicked TreeViewItem is marked as selected by setting the IsItemSelected DependencyProperty to true. After that the TreeViewItem is marked as StartItem within the corresponding TreeView. Therefore the private StartItem DependencyProperty is used. The StartItem DependencyProperty is used as starting point for a subsequent multiple selection where a continuous range is selected.
    private static readonly DependencyProperty StartItemProperty =
        DependencyProperty.RegisterAttached(
            "StartItem",
            typeof(TreeViewItem),
            typeof(TreeViewMultipleSelectionAttached),
            new PropertyMetadata());
 
    private static TreeViewItem GetStartItem(TreeView element)
    {
        return (TreeViewItem)element.GetValue(StartItemProperty);
    }
 
    private static void SetStartItem(TreeView element, TreeViewItem value)
    {
        element.SetValue(StartItemProperty, value);
    }
If a LMB + Ctrl is performed the method SelectMultipleItemsRandomly is called. In this method the IsItemSelected DependencyProperty of the clicked TreeViewItem is toggled. Furthermore the StartItem DependencyProperty will be set of not already set or unset if no TreeViewItem is selected anymore.
    private static void SelectMultipleItemsRandomly(TreeView treeView,
                                                    TreeViewItem treeViewItem)
    {
        SetIsItemSelected(treeViewItem, !GetIsItemSelected(treeViewItem));
        if (GetStartItem(treeView) == null)
        {
            if (GetIsItemSelected(treeViewItem))
            {
                SetStartItem(treeView, treeViewItem);
            }
        }
        else
        {
            if (GetSelectedItems(treeView).Count == 0)
            {
                SetStartItem(treeView, null);
            }
        }
    }
If a LMB + Shift is performed the method SelectMutlipleItemsContinuously is called. If no StartItem is set no action take place. If the selected TreeViewItem is equal to the startItem then the SelectSingleItem method is called and then the method is returned. If both conditions not met mutlitple continuously TreeViewItems selection take place.
    private static void SelectMultipleItemsContinuously(TreeView treeView,
                                                     TreeViewItem treeViewItem)
    {
        TreeViewItem startItem = GetStartItem(treeView);
        if (startItem != null)
        {
            if (startItem == treeViewItem)
            {
                SelectSingleItem(treeView, treeViewItem);
                return;
            }
 
            ICollection<TreeViewItem> allItems = new List<TreeViewItem>();
            GetAllItems(treeView, null, allItems);
            DeSelectAllItems(treeView, null);
            bool isBetween = false;
            foreach (var item in allItems)
            {
                if (item == treeViewItem || item == startItem)
                {
                    // toggle to true if first element is found and
                    // back to false if last element is found
                    isBetween = !isBetween;
 
                    // set boundary element
                    SetIsItemSelected(item, true);
                    continue;
                }
 
                if (isBetween)
                {
                    SetIsItemSelected(item, true);
                }
            }
        }
    }
With the GetAllItems method all TreeViewItems of a TreeView are recursively collected into a collection. Furthermore all items are deselected by the DeSelectAllItems method.
    private static void GetAllItems(TreeView treeView, TreeViewItem treeViewItem,
                                    ICollection<TreeViewItem> allItems)
    {
        if (treeView != null)
        {
            for (int i = 0; i < treeView.Items.Count; i++)
            {
                TreeViewItem item = treeView.ItemContainerGenerator.
                                           ContainerFromIndex(i) as TreeViewItem;
                if (item != null)
                {
                    allItems.Add(item);
                    GetAllItems(null, item, allItems);
                }
            }
        }
        else
        {
            for (int i = 0; i < treeViewItem.Items.Count; i++)
            {
                TreeViewItem item = treeViewItem.ItemContainerGenerator.
                                           ContainerFromIndex(i) as TreeViewItem;
                if (item != null)
                {
                    allItems.Add(item);
                    GetAllItems(null, item, allItems);
                }
            }
        }
    }
Then each element of the list will be checked, if it is in the range of StartItem and selected TreeViewItem. If this is true the TreeViewItem will be marked as selected.


Friday, July 25, 2014

Get Password from WPF PasswordBox with MVVM in a secure and simple way

Keeping cleartext passwords in memory is a security risk. Therefore the Password and SecurePassword property of WPF PasswordBox is not a DependencyProperty and cannot used for binding. To access the password in the ViewModel in a secure way some work have to be done.

At first we define an interface that contains just the SecureString Password as property.
public interface IHavePassword
{
    System.Security.SecureString Password { get; }
}

The View implements the IHavePassword interface. The Property Password in code-behind returns the SecurePassword of the PasswordBox.
public partial class LoginView : UserControlIHavePassword
{
    public LoginView()
    {
        InitializeComponent();
        DataContext = new LoginViewModel();
    }
 
    public System.Security.SecureString Password
    {
        get
        {
            return UserPassword.SecurePassword;
        }
    }
}

In the XAML part of the View the click event of the login button is send by using the RelayCommand pattern that builds on WPF Commanding. Therfore the Command and the CommandParameter properties are set. The View is set to the CommandParameter.
<UserControl x:Class="MvvmPassword.LoginView"
             x:Name="This"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        ...
        <PasswordBox x:Name="UserPassword"
                     Grid.Row="2"
                     Grid.Column="1"
                     Margin="4" />
        
        <Button x:Name="Login"
                Grid.Row="3"
                Grid.ColumnSpan="2"
                Margin="4"
                Content="Login"
                Command="{Binding LoginCommand}"
                CommandParameter="{Binding ElementName=This}"/>
        ...
    </Grid>
</UserControl>
In the ViewModel the LoginCommand is linked to the Login method. This method gets the View as parameter. The view is casted to defined interface IHavePassword. From that we can easily obtain the secure password.
private void Login(object parameter)
{
    var passwordContainer = parameter as IHavePassword;
    if (passwordContainer != null)
    {
        var secureString = passwordContainer.Password;
        PasswordInVM = ConvertToUnsecureString(secureString);
    }
}
The SecureString needs to be converted to string to validate the entered password. There are some pitfalls of converting a SecureString. A nice explanation of them can be found in How to properly convert SecureString to String by Fabio Pintos.
private string ConvertToUnsecureString(SecureString securePassword)
{
   if (securePassword == null)
   {
     return string.Empty;
   }
 
   IntPtr unmanagedString = IntPtr.Zero;
   try
   {
     unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(securePassword);
     return Marshal.PtrToStringUni(unmanagedString);
   }
   finally
   {
     Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString);
   }
}

Monday, June 2, 2014

Pitfalls of using WPF DataGrid

WPF DataGrid is a very powerful Control that was first published within the WPF Toolkit. Since .NET 4.0 the WPF DataGrid is part of the .NET Framework. The simplest way to use it, is to set the ItemsSource, only.
<DataGrid ItemsSource="{Binding Source1}" />
public ICollection<IValue> Source1 { getset; }
Due to the AutoGenerateColumns property, which default value is true, the DataGrid is created with the properties of the class that is used within the collection. In order that this works
  • the binded collection must be public
  • the properties of the used class must be public
For example the interface could look like that
public interface IValue
{
    string Val { get; }
    string Key { get; }
}
and the implementation of the interface could look like that
public class KeyVal : IValue
{
    public KeyVal(string key, string val)
    {
        Key = key;
        Val = val;
    }
 
    public string Val
    {
        get;
        private set;
    }
 
    public string Key
    {
        get;
        private set;
    }
}
All that results into

As you can see columns are auto-generated and filled with the objects of the binded list.
For just viewing collections this is all you have to consider. If you want also add new entries to the collection there must be done some changes. In the XAML part the CanUserAddRows property indicates if it is possible to add new entries. The default value is true, so the XAML part remains unchanged. In code we have to do some changes
  • the generic type of the collection must be a class (not an interface)
  • there must be a default constructor (parameterless) in the generic type of the collection
public ICollection<KeyVal> Source1 { getset; }
public class KeyVal : IValue
{
    public KeyVal()
    {
 
    }
 
    public KeyVal(string key, string val)
    {
        Key = key;
        Val = val;
    }
 
    public string Val
    {
        get;
        set;
    }
 
    public string Key
    {
        get;
        set;
    }
}
Only with these changes a new object can be created from the view and added to the collection. As result there is a new empty entry in the DataGrid that can be filled by the user and is then added to the collection.