jueves, 30 de agosto de 2012

How to get selected Checkboxes in a nested ListView

I'm currently working in a big MVVM WPF application similar to Visual Studio using AvalonDock.
One of the Panels needs to display two nested ListViews:


The first level just displays a List Name, and the second level displays the Values associated to that List Name:


For that I used an Expander with Header bound to the ListName property in my ViewModel and for the nested ListView design I used DataTemplates. Adding Checkboxes to my ListView wasn't a big deal, as explained here: wpf-listview-with-gridviewcolumn-and-datatemplate

Now one of my requirements was being able to check single or multiple values with the Space bar. For single value you just check the highlighted row, for multiple values you need to select a row and press Shift key while moving down or up to select other adjacent rows. I ran into few hurdles when I tried to code that functionality which I will mention... In WPF there are MANY ways and approaches to doing things as you may know.

So my initial question was: how to handle the row selection events fired by my inner ListView checkboxes?? If you understand WPF Routing Strategies and Event Handlers you know how events can be propagated upwards (bubbling), downwards (tunneling)  or direct.

*From WPF 4 Unleashed book:

Routing Strategies and Event Handlers
When registered, every routed event chooses one of three routing strategies—the way in
which the event raising travels through the element tree. These strategies are exposed as
values of a RoutingStrategyenumeration:

  • Tunneling—The event is first raised on the root, then on each element down the tree until the source element is reached (or until a handler halts the tunneling by marking the event as handled).
  • Bubbling—The event is first raised on the source element and then on each element up the tree until the root is reached (or until a handler halts the bubbling by marking the event as handled).
  • Direct—Theevent is raised only on the source element. This is the same behavior as a plain .NET event, except that such events can still participate in mechanisms specific to routed events such as event triggers.
Handlers for routed events have a signature matching the pattern for general .NET event handlers: The first parameter is a System.Object typically named sender, and the second parameter (typically named e) is a class that derives from System.EventArgs. The sender parameter passed to a handler is always the element to which the handler was attached.
The parameter is (or derives from) an instance of RoutedEventArgs, a subclass of EventArgs that exposes four useful properties:
  • Source—The element in the logical tree that originally raised the event.
  • OriginalSource—The element in the visual tree that originally raised the event (forexample, the TextBlockorButtonChromechild of a standard Button).
  •  Handled—A Boolean that can be set to trueto mark the event as handled. This is precisely what halts any tunneling or bubbling.
  • RoutedEvent—The actual routed event object (such as Button.ClickEvent), which can be helpful for identifying the raised event when the same handler is used for multiple routed events.


In my case I decided to handle the PreviewKeyDown event fired by my Checkbox control at the parent ListView level through bubbling.

All I did was added an Event handler in my View constructor:
lv_Main.PreviewKeyDown += lv_Main_KeyDown;

And then added my Event Handling Method as follows:

        
private void lv_Main_KeyDown(object sender, KeyEventArgs e)
{
    if(e.Key==Key.Space)
    {
        ListViewItem lvi = e.OriginalSource as ListViewItem;
        if(lvi != null)
        {
            KeyValuePair<int, RefDataValue> kvp;
            RefDataValue val;
            //Get parent ListView
            ListView parent = ItemsControl.ItemsControlFromItemContainer(lvi) as ListView;
            //Check if parent list view has highlighted elements
            if(parent != null && parent.Items.Count > 0)
            {
                //Loop through a list of selected ListViewItems
                foreach (KeyValuePair<int, RefDataValue> kvp in parent.SelectedItems)
                {
                    val = kvp.Value;
                    val.IsChecked.Val = !val.IsChecked.Val;
                }
                return;
            }
        }
    }
}

It took me a while to come up with this approach but now I understand better how events work in WPF and how you can handle them at different levels. Also I would like to point out that it is not trivial how to obtain the parent ListView of a given ListViewItem. Comming from a WinForms world I expected to use something like mylistview.Parent, but that doesn't work here, you need to use:

        
ListView parent = ItemsControl.ItemsControlFromItemContainer(lvi) as ListView;

Of course, if you're not dealing with a nested ListView all you have to do is access the ListView.SelectedItems() directly. In this case I had to handle the key down event triggered by my ListViewItem deep inside the VisualTree, that is what made it a little more complicated.

It is also important to mention that i am NOT using the pure MVVM pattern here, I am manipulating my ViewModel through the code-behind which is something to be avoided when implementing MVVM. I still need to find a MVVM solution for this problem where my ViewModel is automatically updated through direct WPF binding. This post could provide some ideas on how to do that: How to get selected item of a nested listview?

Hope you find this useful

No hay comentarios:

Publicar un comentario