top of page
avicoren

DataGrid with "Gmail-Like" Controls

Updated: May 29, 2022

I really like the Gmail grid style, where the action buttons are part of the row and how they are floating as the mouse hovers over the row. It's way better than a lot of grids out there, that have dozens of buttons stuck on every row. I think the latter ones have a bad design and ugly looks.


As for checkboxes, well, the days of the checkbox column aren't over yet. It has it's say, especially with older people who are not used to the "select by row click" with control and shift combinations. I guess this is one of the main reasons Gmail is still using it. As they deal with millions of users, their goal is to be user friendly, and have minimum operation complaints from users.



The above demo is using Example17 from my GitHub repo.

Make sure you have both PS1 and Xaml files and the Common files as well.


In order to implement Gmail-Like DataGrid style, first we have to disable the default behavior. It means that we have to ignore the default row selection method and let only the checkboxes do the selection.

The way to do it, is to ignore mouse click selections by adding this event:

$DataGrid.add_SelectionChanged({ $_.Handled = $true })

Also, on the Xaml side we need to configure 'Style Triggers' for both 'DataGridCell' and 'DataGridRow' where we hide the borders and shadows when a user clicks on the row/cell. This will ensure there are no row/cell click effects on the DataGrid, and let the user focus on checkbox row selection.


With the default behavior, we use the DataTable row properties to handle the selected records, but with the checkboxes we have to handled those ourselves. To keep a list of the selected records we define an array, so we can add or remove DataRows according to the user's selections:

[System.Collections.ArrayList]$Script:SelectedRows = @()

On the Xaml side we create a DataGridCheckBoxColumn with a Checkbox control on its header. This Checkbox will let us select or unselect all rows.

Note that a "IsThreeState" attribute is set to true. It will give us the third state indication where some of the records are selected.

Now comes the three header Checkbox events. One for each state:

$Services_HeaderChkBox.add_Checked
$Services_HeaderChkBox.add_UnChecked
$Services_HeaderChkBox.add_Indeterminate

All three events check to see if the mouse was over the header Checkbox when the checkbox's state was changed, meaning that the user has clicked on it.

After the checked state, comes the Indeterminate state (represented by a minus sign). We don't want the user to get to this state by clicking the header Checkbox, so we change its state to unselect. This way only two states are clickable - either Checked or Unchecked. When it's checked - we clear the list and then add all rows to the list, and when it's unchecked we just clear the list. We also change the state of all row checkboxes accordingly.

Note, that the row checkboxes are a DataTable column with a 'Bool' type, so it accepts only $true or $false and the bounded checkbox will change its state according to the row values in our 'CheckboxSelect' column.


Selecting and unselecting all rows is easy. Selecting individual rows is a bit tricky. On the Xaml DataGrid we define the row checkbox with 'ElementStyle' as we want it to have a Material Design style. We are not putting a row Checkbox element on Xaml so we cannot name it, thus, no PowerShell side object is created for this Checkbox! It has some consequences, such as we cannot define events to the row Checkboxes. As we already know, without events the app is useless, so how can we compensate on that? Well, we can. By using the DataGrid's 'GotMouseCapture' event, we can isolate the user's specific actions, and find the ones related to our Checkboxes.

Using the $_.OriginalSource value, we can find out what control was clicked when GotMouseCapture was triggered. If it's a Checkbox (and it's NOT the header one) then we can decide what to do, based on the Checkbox state. One drawback is that GotMouseCapture is triggered BEFORE the Checkbox state was actually changed! which forces us to artificially flip it one more time to get to the state the user wanted. Also, our 'Is_Checked' value is also reversed, i.e. true is false and vice versa, so be carful with your condition statements.

We also count the number of selected items in our list and compare it to the total number of items in the DataTable. if we reach that number we check the header Checkbox and when it's 0 we uncheck it. Any other number of records means we have to set the header Checkbox to an Indeterminate state.


We are using the GotMouseCapture event also for the floating buttons. I will cover it in a minute, but first let's talk about the floating buttons Xaml code.

For every button, we create a DataGridTemplateColumn with its content inside a DataTemplate.

Now, using a DataTemplate in PowerShell is very problematic when it comes to adding events to controls you put in it.

As we know, for every control in Xaml, a variable will be created in the PS side, as long as we put 'Name=' as an attribute to it, right? Also this variable will be bound to the control, so whenever we change a property in the code, it will render it in the displayed window.

Well, the problem is that while all of the elements inside a 'DataTemplate' will have a variable in the PS code, these variables will have a $null value, thus will not be bound to anything.

For example, if we want to put a button in a 'DataGridTemplateColumn' we will define it like this:

<DataGrid... >
    <DataGrid.Columns>
        <DataGridTemplateColumn ... >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <Button Name="Btn1" Content="Click Me" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

When the program runs, a variable named 'Btn1' will be created but it's value will be null and if we try to add an event to it, like:

$Btn1.add_Click({ Do somthing })

We will get an error when we try to run the program, because PS cannot add events to null objects.


Such a problem does not exist in WPF apps using C# It somehow happens only with PowerShell. I couldn't find any elegant solution for it online, not even on Microsoft's documentation.

What usually developers are doing, is building the column in PS code and not in Xaml, but it is a partial solution. The problem with this solution is that the elements are locked and you cannot change their state at runtime. i.e. if you want to check a toggle button programmatically - It won't let you.

An example for defining a column in code (ClickEvent code not included):

$buttonColumn = New-Object System.Windows.Controls.DataGridTemplateColumn

$buttonFactory = New-Object System.Windows.FrameworkElementFactory([System.Windows.Controls.Button])
    $buttonFactory.SetValue([System.Windows.Controls.Button]::ContentProperty, "Launch")

$buttonFactory.AddHandler([System.Windows.Controls.Button]::ClickEvent,$clickEvent)

$dataTemplate = New-Object System.Windows.DataTemplate
$dataTemplate.VisualTree = $buttonFactory
$buttonColumn.CellTemplate = $dataTemplate
$myGrid.Columns.Add($buttonColumn)
$myGrid.DataContext = $table.DefaultView

So because of the restriction that exist with this solution I am not using it, instead I'm defining the control in a DataTemplate in Xaml and use the same old

GotMouseCapture DataGrid event to check for $_.OriginalSource.

When the DataTemplate button will be clicked, the statement:

($_.OriginalSource).GetType().name 

will return the string "Button". Also the $_.OriginalSource.Name value will return the actual name of the button as defined in Xaml. Nice!

From this point I can handle the button event just as a 'add_Click' event.


Last thing I would like to discuss is the way the buttons are "floating".

The trick is to set their visibility to "Hidden" and define a DataTemplate Trigger that is bound to the row's mouse over. Whenever the mouse is over a row, the button's visibility is changed to "Visible". Case closed.


I think that this type of DataGrid, can let you have full control over the things a user can do on it, plus the guarantee that you won't get phone calls from users asking you how to use it. Enjoy.





1,672 views1 comment

Recent Posts

See All

1 Comment


Guest
Jul 30, 2023

This is a really useful blog post and the DataGrid is almost perfect for a project I’m currently working on.


One question I had. how would one go about disabling the button on a given row, depending on criteria from that same row?

Essentially; I only want the button to be shown in the UI for a given row if another attribute in the same row has a specific value. Can this be done?

Like
bottom of page