Friday, February 1, 2019

Binding to SelectedDataItems on XamDataGrid

Unlike the Microsoft data grid the Infragistics XamDataGrid does support binding to the SelectedDataItems property. However, doing so is not as straight forward as you might hope. The SelectedDataItemsScope has a lot of effect and the binding is a bit weird.

Here's a fun project that demostrates how to do the binding and the effect of this property.

Start a new C# project in Visual Studio targeting any framework you want. Call it DataItemsBinding.


We will create a grid containing a combo box that lists the SelectedDataItemsScope enumeration and then a grid that lists all my bicycles. At the bottom is a Text Block that shows how many bicycles are selected.


<Window x:Class="DataItemsBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:DataItemsBinding"
        xmlns:igDP="http://infragistics.com/DataPresenter"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Data Items Binding" Height="300" Width="400">
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Row="0" Grid.Column="0" Text="SelectedDataItemsScope:"/>
        <ComboBox Grid.Row="0" Grid.Column="1"  ItemsSource="{Binding SelectedDataItemScopes}" SelectedItem="{Binding SelectedSelectedDataItemsScope}" IsEditable="False"/>

        <igDP:XamDataGrid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="0,20,0,0" DataSource="{Binding Bicycles}" SelectedDataItems="{Binding SelectedBicycles}" SelectedDataItemsScope="{Binding SelectedSelectedDataItemsScope}" GroupByAreaLocation="None">
            <igDP:XamDataGrid.FieldLayoutSettings>
                <igDP:FieldLayoutSettings SelectionTypeRecord="Extended" AutoGenerateFields="False"/>
            </igDP:XamDataGrid.FieldLayoutSettings>
            <igDP:XamDataGrid.FieldSettings>
                <igDP:FieldSettings AllowSummaries="False" AllowEdit="False" CellClickAction="SelectRecord"/>
            </igDP:XamDataGrid.FieldSettings>
            <igDP:XamDataGrid.FieldLayouts>
                <igDP:FieldLayout>
                    <igDP:TextField Label="Bicycle Type" Name="Type" Width="Auto"/>
                    <igDP:TextField Label="Brand" Name="Brand" Width="Auto"/>
                    <igDP:TextField Label="Year" Name="Year" Width="Auto"/>
                </igDP:FieldLayout>
            </igDP:XamDataGrid.FieldLayouts>
        </igDP:XamDataGrid>

        <TextBlock Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding SelectedBicycles.Length, StringFormat='Selected Count = {0}'}"/>
    </Grid>
</Window>

----------------------------------------------------------------


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using Infragistics.Windows.DataPresenter;

namespace DataItemsBinding
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {

        public IEnumerable<SelectedDataItemsScope> SelectedDataItemScopes
        {
            get { return Enum.GetValues(typeof(SelectedDataItemsScope)).Cast<SelectedDataItemsScope>(); }
        }

        private SelectedDataItemsScope _SelectedSelectedDataItemsScope;
        public SelectedDataItemsScope SelectedSelectedDataItemsScope
        {
            get { return _SelectedSelectedDataItemsScope; }
            set { SetProperty(ref _SelectedSelectedDataItemsScope, value); }
        }

        public class cBicycle
        {
            public string Type { get; set; }
            public string Brand { get; set; }
            public string Year { get; set; }
        }

        private List<cBicycle> _Bicycles;
        public List<cBicycle> Bicycles
        {
            get { return _Bicycles; }
            set { SetProperty(ref _Bicycles, value); }
        }

        private List<cBicycle> _SelectedBicycles = new List<cBicycle>();
        public object[] SelectedBicycles
        {
            get { return _SelectedBicycles.ToArray(); }
            set { SetProperty(ref _SelectedBicycles, value.ToList().Cast<cBicycle>().ToList()); }
        }

        public MainWindow()
        {
            Bicycles = new List<cBicycle>()
            {
                new cBicycle() {Type="Road", Brand="Serotta", Year="1998"},
                new cBicycle() {Type="Exercise", Brand="Schwinn", Year="2016"},
                new cBicycle() {Type="Mountain", Brand="Fisher", Year="2002"},
                new cBicycle() {Type="Touring", Brand="Trek", Year="2014"},
                new cBicycle() {Type="Classic", Brand="Bianchi", Year="1975"}
            };
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Object.Equals(storage, value)) return false;
            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }
        private void OnPropertyChanged(String PropertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
            }
        }
    }
}

Take a look at how we bind a dropdown list to an enumeration. This is a handy trick to know.
public IEnumerable<SelectedDataItemsScope> SelectedDataItemScopes
{
    get { return Enum.GetValues(typeof(SelectedDataItemsScope)).Cast<SelectedDataItemsScope>(); }
}
The rest of the code is quite mundane - properties, classes, more properties, etc.

Now run the application. The default setting for SelectedDataItemsScope is None which means that the SelectedDataItems is never updated, which is an odd design decision. Change SelectedDataItemsScope to RecordsOnly and then select some bicycles - you will see the count of selected bicycles updated below the grid.


Note that selected rows are only indicated by a slight change to the background color and a faint border. By default, the selection indicator to the left only indicates the last selected row.

The SelectedDataItems property is an array of objects. You have to do some work to get them back into a list of bicycles and more work to convert a list of bicycles back to an array.

private List<cBicycle> _SelectedBicycles = new List<cBicycle>();
public object[] SelectedBicycles
{
    get { return _SelectedBicycles.ToArray(); }
    set { SetProperty(ref _SelectedBicycles, value.ToList().Cast<cBicycle>().ToList()); }
}

Try setting CellClickAction="SelectCell" and then selecting "RecordsOrCells" in the drop down list to see how that option works.   


Thursday, January 17, 2019

Re-evaluating RoutedCommands from code

I have a requirement to launch a background process that, when it finishes, creates a condition that allows a command to be executed. I need a way to tell WPF to re-evaluate a command from code. Normally this happens only when the user types or clicks the mouse.

The trick is to use Windows.Input.CommandManager.InvalidateRequerySuggested()

Here's an overly complicated example. Start a new WPF App project using C# and call it EvaluateCommand.


The main window will have two buttons and a TextBlock. Each button is linked to a command that is only executable when the TextBlock has a specific content. Each button launches a background task that changes the content of the TextBlock. We need that task to cause the commands' CanExecute methods to be re-evaluated when the task is done.

Here is the XAML

<Window x:Class="EvaluateCommand.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:EvaluateCommand"
        mc:Ignorable="d"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        Title="Evaluate Command" SizeToContent="WidthAndHeight">
    <Window.Resources>
        <RoutedCommand x:Key="FailCommand"/>
        <RoutedCommand x:Key="PassCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource FailCommand}" CanExecute="Fail_CanExecute" Executed="Fail_Executed"/>
        <CommandBinding Command="{StaticResource PassCommand}" CanExecute="Pass_CanExecute" Executed="Pass_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="auto"/>
        </Grid.ColumnDefinitions>
        <Button Grid.Column="0" Content="Make it Fail" Command="{StaticResource FailCommand}" Width="100" Margin="20"/>
        <TextBlock Grid.Column="1" Text="{Binding Status}" Width="100" Margin="20"/>
        <Button Grid.Column="2" Content="Make it Pass" Command="{StaticResource PassCommand}" Width="100" Margin="20"/>
    </Grid>
</Window>


Here's the code. Note InvalidateRequerySuggested cannot be called from the background worker thread. It must be called from the UI thread once the background worker has finished. The sleep is to make it obvious the [Make it Pass] button is not being re-evaluated from the initial button click.

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Runtime.CompilerServices;

namespace EvaluateCommand
{

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private string _Status = "PASS";

        public string Status
        {
            get { return _Status; }
            set { SetProperty(ref _Status, value); }
        }
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Fail_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (Status == "PASS");
        }

        private void Fail_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            SetStatusAsync("FAIL");
        }

        private void Pass_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (Status == "FAIL");
        }

        private void Pass_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            SetStatusAsync("PASS");
        }

        private void SetStatusAsync(String NewStatus)
        {
            Status = "Processing...";
            BackgroundWorker BW = new BackgroundWorker();
            BW.DoWork += SetStatus;
            BW.RunWorkerCompleted += EvaluateCommands;
            BW.RunWorkerAsync(NewStatus);
            BW.Dispose();
        }

        private void SetStatus(object sender, DoWorkEventArgs e)
        {
            System.Threading.Thread.Sleep(1000);
            Dispatcher.Invoke(new Action(() => Status = e.Argument.ToString()));
        }

        private void EvaluateCommands(object sender, RunWorkerCompletedEventArgs e)
        {
            System.Windows.Input.CommandManager.InvalidateRequerySuggested();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Object.Equals(storage, value)) return false;
            storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }
        private void OnPropertyChanged(String PropertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
            }
        }

    }
}