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()
{
//
}
}
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!