7

Short explanation of need: I need to fire the command of a button inside a DataTemplate, using a method from the DataContext of the ViewModel.

Short explanation of problem: The templated button command only seems to be bindable to the datacontext of the item itself. The syntax used by WPF and Windows 8.1 apps to walk up the visual tree doesn't seem to work, including ElementName and Ancestor binding. I would very much prefer not to have my button command located inside the MODEL.

Side Note: This is built with the MVVM design method.

The below code generates the list of items on the VIEW. That list is one button for each list item.

            <ItemsControl x:Name="listView" Tag="listOfStories" Grid.Row="0" Grid.Column="1"
            ItemsSource="{x:Bind ViewModel.ListOfStories}"
            ItemTemplate="{StaticResource storyTemplate}"
            Background="Transparent"
            IsRightTapEnabled="False"
            IsHoldingEnabled="False"
            IsDoubleTapEnabled="False"
                 />

Inside the page resources of the same VIEW, I have created a DataTemplate, containing the problematic button in question. I went ahead and stripped out most of the formatting inside the button, such as text, to make the code easier to read on this side. Everything concerning the button works, except for the problem listed, which is the binding of the command.

<Page.Resources>
        <DataTemplate x:Name="storyTemplate" x:DataType="m:Story">
            <Button
                Margin="0,6,0,0"
                Width="{Binding ColumnDefinitions[1].ActualWidth, ElementName=storyGrid, Mode=OneWay}"
                HorizontalContentAlignment="Stretch"
                CommandParameter="{Binding DataContext, ElementName=Page}"
                Command="{Binding Source={StaticResource Locator}}">

                <StackPanel HorizontalAlignment="Stretch" >
                    <TextBlock Text="{x:Bind StoryTitle, Mode=OneWay}"
                        FontSize="30"
                        TextTrimming="WordEllipsis"
                        TextAlignment="Left"/>
                </StackPanel>
            </Button>
        </DataTemplate>
    </Page.Resources>

Because this is a DataTemplate, the DataContext has been set to the individual items that comprise the list (MODEL). What I need to do is select the DataContext of the list itself (VIEWMODEL), so I can then access a navigation command.

If you are interested in the code-behind of the VIEW page, please see below.

    public sealed partial class ChooseStoryToPlay_View : Page
    {
    public ChooseStoryToPlay_View()
    {
        this.InitializeComponent();
        this.DataContextChanged += (s, e) => { ViewModel = DataContext as ChooseStoryToPlay_ViewModel; };
    }
    public ChooseStoryToPlay_ViewModel ViewModel { get; set; }
}

I've tried setting it by ElementName, among many other attempts, but all have failed. Intellisense detects "storyTemplate" as an option when ElementName is input, which is the name of the DataTemplate shown in the first code block of this question.

I don't believe my problem can be unique, however I'm having great difficulty finding a solution for UWP. Allow me to apologize in advance in this is a simple question, but I've spent nearly two days researching answers, with none seeming to work for UWP.

Thank you guys!

RAB
  • 317
  • 3
  • 12
  • I have never tried UWP, but in other XAML based frameworks it is usually handled with [RelativeSource](http://stackoverflow.com/questions/1127933/wpf-databinding-how-do-i-access-the-parent-data-context). If it won't help then it is either due to the DataTemplate being in the resources (though it is doubtful) or UWP XAML having some particularities of its own. – Eugene Podskal Jan 23 '16 at 20:51
  • Yes sir, that's what has led to such frustration for me. The first thing I did was a RelativeSource ancestor binding, just like I've done on past platforms, but this time it didn't work. Thank you. – RAB Jan 23 '16 at 21:01

3 Answers3

8

What MVVM toolkit are you using (if any)? In MVVM Light, you can get a hold of ViewModel from DataTemplate same way you set DataContext for your view:

<DataTemplate x:Key="SomeTemplate">
    <Button Command="{Binding Main.MyCommand, Source={StaticResource ViewModelLocator}}"/>
</DataTemplate>
Andrei Ashikhmin
  • 2,401
  • 2
  • 20
  • 34
  • Would this instantiate a new, disconnected version of the ViewModel? Thank you for answering! – RAB Jan 24 '16 at 06:48
  • No, it will not. In your ViewModelLocator Main supposed to just get instance of MainViewModel from default ServiceLocator, which returns singleton. [Some more info](http://stackoverflow.com/a/13805067/2279177). – Andrei Ashikhmin Jan 24 '16 at 06:52
  • *slaps forehead* I feel like a complete idiot right now. Syn, please allow me to thank you most sincerely. I'm bleary eyed from looking at this code but I think your code will work! – RAB Jan 24 '16 at 06:56
  • Make sure you got everything right in your Locator, and it will work like a charm. Glad to help. – Andrei Ashikhmin Jan 24 '16 at 06:57
  • BTW, love your Avatar. This is Commander Shepard, and I approve your answer. – RAB Jan 24 '16 at 06:58
  • brilliant work. It was Source={StaticResource Locator} for me encase people are using MVVMLight examples – Clinton Ward May 23 '16 at 10:10
3

It really is unfortunate that there is no ancestor binding in UWP. This makes scenarios like yours much more difficult to implement.

The only way I can think of is to create a DependencyProperty for ViewModel on your Page:

public ChooseStoryToPlay_ViewModel ViewModel
{
    get { return (ChooseStoryToPlay_ViewModel)GetValue(ViewModelProperty); }
    set { SetValue(ViewModelProperty, value); }
}

public static readonly DependencyProperty ViewModelProperty =
    DependencyProperty.Register("ViewModel", typeof(ChooseStoryToPlay_ViewModel), typeof(MainPage), new PropertyMetadata(0));

Now you can bind to it from your data template:

<DataTemplate x:Name="storyTemplate" x:DataType="local:Story">
    <Button
        Margin="0,6,0,0"
        Width="{Binding ColumnDefinitions[1].ActualWidth, ElementName=storyGrid, Mode=OneWay}"
        HorizontalContentAlignment="Stretch"
        CommandParameter="{x:Bind Page}"
        Command="{Binding ViewModel.NavigateCommand, ElementName=Page}">

        <StackPanel HorizontalAlignment="Stretch" >
            <TextBlock Text="{x:Bind StoryTitle, Mode=OneWay}"
                FontSize="30"
                TextTrimming="WordEllipsis"
                TextAlignment="Left"/>
        </StackPanel>
    </Button>
</DataTemplate>

A couple of things to notice:

  • In CommandParameter I assumed that in your Story class there is a Page property that you want to pass as a parameter to your command. You can bind to any other property of Story class here or the class itself.
  • You have to set the name of your page to Page (x:name="Page"), so that you can reference it using ElementName in the data template.
  • I assumed that the command you're calling on the ViewModel is named NavigateCommand and accepts a parameter of the same type as the property bound to CommandParameter:

    public ICommand NavigateCommand { get; } = 
        new RelayCommand<string>(name => Debug.WriteLine(name));
    

I hope this helps and is applicable to your scenario.

Damir Arh
  • 17,637
  • 2
  • 45
  • 83
  • That is fantastic. Thank you so much for settling the matter of whether or not ancestor binding is available UWP. I'd pretty much decided that in the absence of being able to bind to my view model command, two options were available. Create a command on the MODEL, and pass in the story as a parameter, which I am loathe to do...or to apply a click parameter to the button on the VIEW backend. Small rant here, but I find it incomprehensible that a platform Microsoft touts as the future for programs strips out that functionality. Thank you again, Damir! – RAB Jan 24 '16 at 06:42
  • This doesn't help if the DataContext is in a ResourceDictionary. Has anyone found a proper solution? – Michael Puckett II Mar 22 '17 at 17:19
0

There is a few ways to do that. But i think the Command change better...

Example, you have a (grid,list)view with some itemtemplate like that:

                    <GridView.ItemTemplate>
                        <DataTemplate>

                            <Grid
                                    x:Name="gdVehicleImage"
                                    Height="140"
                                    Width="140"
                                    Background="Gray"
                                    Margin="2"

                                >

                           </Grid>

                 </GridView.ItemTemplate>

And do you want to make a command to for example a FlyoutMenu... But the command it's in the ViewModel and not in GridView.SelectedItem...

What you can do is...

                        <Grid
                                    x:Name="gdVehicleImage"
                                    Height="140"
                                    Width="140"
                                    Background="Gray"
                                    Margin="2"

                                >

                                <FlyoutBase.AttachedFlyout>
                                    <MenuFlyout
                                            Opened="MenuFlyout_Opened"
                                            Closed="MenuFlyout_Closed"
                                        >

                                        <MenuFlyout.MenuFlyoutPresenterStyle>
                                            <Style TargetType="MenuFlyoutPresenter">
                                                <Setter Property="Background" Value="DarkCyan"/>
                                                <Setter Property="Foreground" Value="White"/>
                                            </Style>
                                        </MenuFlyout.MenuFlyoutPresenterStyle>

                                        <MenuFlyoutItem 

                                                Loaded="mfiSetAsDefaultPic_Loaded" 
                                                CommandParameter="{Binding}"
                                                />
                                        <MenuFlyoutItem 

                                                Loaded="mfiDeletePic_Loaded" 
                                                CommandParameter="{Binding}"
                                                />

                                    </MenuFlyout>
                                </FlyoutBase.AttachedFlyout>


             </Grid>

And in the loaded events:

    private void mfiDeletePic_Loaded(object sender, RoutedEventArgs e)
    {
        var m = (MenuFlyoutItem)sender;

        if (m != null)
        {

            m.Command = Vm.DeleteImageCommand;
            //Vm is the ViewModel instance...
        }
    }

Is not entirely beautiful... But you willnot breake mvvm pattern like this...

Breno Santos
  • 110
  • 1
  • 8
  • I really appreciate you taking the time read through my question and put that code together. I'm afraid it's not super helpful for my application though. I only need to fire button command to activate a backing command on the viewmodel that navigates to another page, while carrying over any parameters. Thank you again, though! – RAB Jan 24 '16 at 04:41
  • Yes, just think you button like flyoutmenu... Put your button Command in loaded event but keep CommandParameter with binding... And u will have your command with parameter to navigate to another page :D – Breno Santos Jan 24 '16 at 06:30