How to execute command from GridViewItem Tap event (XAML)
Asked Answered
I

4

9

I am trying to follow the MVVM pattern in my Windows 8.1 store app (XAML).

I want to navigate to a new view when a GridViewItem is clicked / tapped in the UI. I wanted to do this without code behind events to promote testability (using MVVM Light).

In order to allow my UI to bind to a view model command I have been looking at the Microsoft Behaviors SDK (XAML) added via Add References -> Windows -> Extensions.

The following code in my view compiles but blows up when I tap the grid view item. Unfortunately it offers little help & just throws an unhandled win32 exception [3476].

Can somebody please help shed some light on the problem?

Namespaces used are;

xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:core="using:Microsoft.Xaml.Interactions.Core"


<GridView x:Name="itemGridView"
                      AutomationProperties.AutomationId="ItemGridView"
                      AutomationProperties.Name="Grouped Items"            
                      ItemsSource="{Binding Source={StaticResource GroupedSource}}"                         
                      IsSwipeEnabled="True"
                      IsTapEnabled="True">

                <GridView.ItemTemplate>
                    <DataTemplate>
                        <Grid Margin="0"
                              Height="230">
                            <StackPanel Orientation="Vertical"
                                        HorizontalAlignment="Stretch">
                                <Image Source="{Binding Image}"                                  
                                       Stretch="UniformToFill"
                                       HorizontalAlignment="Center"
                                       VerticalAlignment="Center"
                                       />
                                <StackPanel VerticalAlignment="Bottom"
                                            Height="45"
                                            Margin="0,-45,0,0">
                                    <StackPanel.Background>
                                        <SolidColorBrush Color="Black" 
                                                         Opacity="0.75" 
                                                         />
                                    </StackPanel.Background>
                                    <TextBlock FontSize="16"
                                               Margin="2"
                                               Text="{Binding Name}"
                                               TextWrapping="Wrap"
                                               VerticalAlignment="Bottom"
                                               />
                                </StackPanel>
                            </StackPanel>

                            <interactivity:Interaction.Behaviors>
                                <core:EventTriggerBehavior EventName="Tapped">
                                    <core:InvokeCommandAction Command="{Binding DataContext.SummaryCatagorySelectedCommand, ElementName=LayoutRoot}" />
                                </core:EventTriggerBehavior>                                                                        
                            </interactivity:Interaction.Behaviors>                                                                                             
                        </Grid>
                    </DataTemplate>
                </GridView.ItemTemplate>

Edit. As requested, I've added the view model, containing specifically the command I want to fire from my behavior.

public class ViewModel : ViewModelBase
{
    public RelayCommand<string> SummaryCatagorySelectedCommand { get; set; }

    public ViewModel()
    {
        //
    }
}
Icelander answered 12/11, 2013 at 21:24 Comment(1)
So what is the viewModel and where have you bind with viewmodel with view?Michaelson
L
13

The simplest answer is to tell you that you should not use a command in this situation. First, the value of a command is that it both executes and communicates back the inability to execute to the interactive XAML control. For example, the button is disabled when the command is not available.

But since you are using the tapped event of a framework element, you are basically just using the control as if it is a simple method, and not a command at all. Your view model can have both commands and methods, of course. And behaviors can call both commands and methods.

To that end, the best scenario here for your solution is to change your approach from calling a command in your view model. Your difficulties are 1. the command is out of scope of the data template and 2. the command parameter is passed inside an out of scope threading context.

Here's what I would suggest to make your life easier and your app simpler.

Do not attach to the tapped event of the item. But instead attach to the itemclicked event of the gridview. This, of course, means you need to set IsItemClickEnabled to true on your gridview. Then don't call to a command, which is overhead you are not using, but instead call to a method.

This is what the method would look like in your viewmodel:

public async void ClickCommand(object sender, object parameter)
{
    var arg = parameter as Windows.UI.Xaml.Controls.ItemClickEventArgs;
    var item = arg.ClickedItem as Models.Item;
    await new MessageDialog(item.Text).ShowAsync()
}

The name of the method does not matter (I even called it a Command to make the point), but the signature does. The behavior framework is looking for a method with zero parameters or with two object-type parameters. Conveniently, the two parameter version gets the event signature forwarded to it. In this case, that means you can use the ItemClickEventArgs which contains the clicked item. So simple.

Your gridview is simplified, too. Instead of trying to force the scope inside your data context you can simply reference the natural scope of the gridview to the outer viewmodel. It would look something like this:

<GridView Margin="0,140,0,0" Padding="120,0,0,0" 
            SelectionMode="None" ItemsSource="{Binding Items}" 
            IsItemClickEnabled="True">
    <Interactivity:Interaction.Behaviors>
        <Core:EventTriggerBehavior EventName="ItemClick">
            <Core:CallMethodAction MethodName="ClickCommand" 
                TargetObject="{Binding Mode=OneWay}" />
        </Core:EventTriggerBehavior>
    </Interactivity:Interaction.Behaviors>

It's such a simpler solution, and doesn't violate anything in the MVVM pattern because it still pushes the logic out to your separated and testable viewmodel. It lets you effectively use behaviors as an event-to-command but actually using a simpler event-to-method pattern. Since the behavior doesn't pass the CanExecute value back to the control in the first place, this actually simplifies your viewmodel, too.

If, it turns out what you are wanting to do is reuse an existing command that is already leveraged elsewhere (which sounds like the 1% edge case) you can always create a shell method for this purpose that internally leverages the command for you.

As a warning, the RelayCommand that ships with Windows 8.1 does not properly implement ICommand as it does not first test CanExecute before Execute is invoked. In addition, the CanExecute logic in the typed RelayCommand does not pass the CommandParameter to the handler. None of this really matters, depending on who you are using commands in the first place. It matters to me though.

So, that's your answer. Change to GridView.ItemClicked and change from ViewModel.Command to ViewModel.Method. That makes your life easier, far easier, and makes your XAML more portable should you ever want to reuse your data template.

Best of luck!

Lactoprotein answered 19/11, 2013 at 23:59 Comment(3)
Sorry for the delay in replying. I agree I think that makes sense. Thanks.Icelander
This answer just helped me tremendously when trying to find a good way to bind navigation to my ViewModel rather than in the View. Using Behaviors and binding to the Method was exactly what I needed. Thanks!Broider
@jerry-nixon-msft This causes issue when trying to navigate between pages. Enable caching on page1. Use command to navigate to page2. When you navigate back to page1, two method calls are attached to the ItemClick event instead of one. This results into 2 navigation calls next time an item is tapped.Abyss
F
0

When you tap on any item you change SelectedItem, I guess. You can Binding (Mode=TwoWay) SelectedItem and in Set() of property raise needed action.

Or you can use something like this and use as dependency property of your GridView.

public class GridViewItemClickCommand
{
    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.RegisterAttached("Command", typeof(ICommand),
        typeof(GridViewItemClickCommand), new PropertyMetadata
(null, CommandPropertyChanged));


    public static void SetCommand(DependencyObject attached, ICommand value)
    {
        attached.SetValue(CommandProperty, value);
    }


    public static ICommand GetCommand(DependencyObject attached)
    {
        return (ICommand)attached.GetValue(CommandProperty);
    }


    private static void CommandPropertyChanged(DependencyObject d,
                                    DependencyPropertyChangedEventArgs e)
    {
        // Attach click handler
        (d as GridView).ItemClick += gridView_ItemClick;
    }


    private static void gridView_ItemClick(object sender,
                                           ItemClickEventArgs e)
    {
        // Get GridView
        var gridView = (sender as GridView);


        // Get command
        ICommand command = GetCommand(gridView);


        // Execute command
        command.Execute(e.ClickedItem);
    }
}

If you have any problems, please ask :)

Folks answered 13/11, 2013 at 19:6 Comment(1)
Thanks for the answer. I suppose I could always go this way. Perhaps I should if it's easier. I really wanted to understand what was wrong with my use of Behaviors though if possible.Icelander
J
0

I had the same issued but I solved it in another way. I don't put the behavior insie the datatemplate, I do it in the GridView :

 <GridView x:Uid="Flow" 
                x:Name="PlacesGridView"
                Grid.Column="1" Grid.Row="2"
                ItemTemplate="{StaticResource YourDataTemplate}" 
                ItemsSource="{Binding YourSource}" >
                <Interactivity:Interaction.Behaviors>
                    <Behaviors:GoToDestinationOnSelected/>
                </Interactivity:Interaction.Behaviors>
            </GridView>

This is how GoToDestinationOnSelected looks like:

 public class GoToDestinationOnSelected : DependencyObject, IBehavior
{
   .....

    void GoToDestinationOnGridViewItemSelected_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        object obj = (sender as ListViewBase).SelectedItem;

        if (obj == null)
            return;

        if (obj is YourClass)
        {
                App.RootFrame.Navigate(typeof(CountryPlacesPage));
                return;


        ((AssociatedObject) as ListViewBase).SelectedIndex = -1;
    }

    public DependencyObject AssociatedObject
    {
        get;
        private set; 
    }

    public void Attach(DependencyObject associatedObject)
    {
        AssociatedObject = associatedObject;
                  (associatedObject as ListViewBase).SelectionChanged += GoToDestinationOnGridViewItemSelected_SelectionChanged;
    }

    public void Detach()
    {
        (AssociatedObject as ListViewBase).SelectionChanged -= GoToDestinationOnGridViewItemSelected_SelectionChanged;   
    }

    ~GoToDestinationOnSelected()
    {

    }
Jefe answered 15/11, 2013 at 12:35 Comment(0)
Z
0

In UWP(Window 10 and newer) Mobile App, apply the below code snippet

<Interactivity:Interaction.Behaviors>
                                    <Core:EventTriggerBehavior EventName="ItemClick">
                                        <Core:EventTriggerBehavior.Actions>
                                            <Core:`enter code here`InvokeCommandAction Command="{Binding itemclick}"/>
                                        </Core:EventTriggerBehavior.Actions>
                                    </Core:EventTriggerBehavior>
                                </Interactivity:Interaction.Behaviors>
Zebulun answered 16/11, 2016 at 7:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.