简体   繁体   中英

Bind a WPF RelayCommand from DataTemplate to a Button inside a UserControl

I searched quite a while to solve this issue with RelayCommands but could not find a similar solution.

The issue is that I have a UserControl , and in this UserConrol is a button among others ( btcNotKnown towards the end of the code):

<UserControl x:Class        = "Vokabelizer.Views.viewLearnSpeak"
         xmlns          = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x        = "http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc       = "http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d        = "http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local    = "clr-namespace:Vokabelizer.Views"
         xmlns:conv     = "clr-namespace:Vokabelizer.Global.Converter"
         xmlns:ccont    = "clr-namespace:Vokabelizer.Controls;assembly=Vokabelizer.Controls"
         mc:Ignorable   = "d" 
         d:DesignHeight = "300"
         d:DesignWidth  = "800"
         Height         = "300"
         x:Name         = "root">

<UserControl.Resources>
    ...
</UserControl.Resources>

<Grid Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="0">

    <Grid.RowDefinitions>
        ...
    </Grid.RowDefinitions>

    <!-- The question to answer -->
    <Border Grid.Column         = "0"
            Grid.Row            = "0"
            Padding             = "5"
            HorizontalAlignment = "Stretch"
            VerticalAlignment   = "Stretch">

        <TextBlock Text                 = "{Binding Path=LessonNative}"
                   Style                = "{StaticResource UIText}"/>

    </Border>

    <!-- Validation content -->
    <Border Grid.Column         = "0" 
            Grid.Row            = "1"
            Padding             = "5">

        <ToggleButton x:Name        = "QnAToggle"
                      Command       = "{Binding Path=cmdValidateOK}"
                      IsThreeState  = "False">

            <ToggleButton.Style>
                <Style TargetType="ToggleButton" BasedOn="{StaticResource ValidateToggle}">
                    <Style.Triggers>
                        <Trigger Property="IsChecked" Value="False">
                            <Setter Property="Content">
                                <Setter.Value>
                                    <TextBlock Text     = "?"
                                               Style    = "{StaticResource UIText}" />
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                        <Trigger Property="IsChecked" Value="True">
                            <Setter Property="Content">
                                <Setter.Value>
                                    <TextBlock Text     = "{Binding Path=LessonForeign}"
                                               Style    = "{StaticResource UIText}" />
                                </Setter.Value>
                            </Setter>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ToggleButton.Style>

        </ToggleButton>

    </Border>

    <!-- Result Evaluation buttons -->
    <Border Grid.Column         = "0"
            Grid.Row            = "2">

        <Grid>

            <Grid.ColumnDefinitions>
                ...
            </Grid.ColumnDefinitions>

            <ccont:vokButton x:Name         = "btcNotKnown"
                             Command        = "{Binding Command, ElementName=root}"
                             Grid.Column    = "0"
                             Grid.Row       = "0"
                             Content        = "Not Known"
                             Corner         = "5"
                             Style          = "{StaticResource ValidateNotKnown}"
                             Visibility     = "{Binding ElementName=QnAToggle, Path=IsChecked, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=hidden}" />

        </Grid>

    </Border>

</Grid>

In the code behind of the UserControl I defined a DependencyProperty to expose the command from the UserControl and hence bind to the Button in the UserControls xaml:

public partial class viewLearnSpeak : UserControl
{
    public viewLearnSpeak()
    {
        InitializeComponent();
    }

    #region DepProp: Command

    public static readonly DependencyProperty CommandProperty = DependencyProperty.Register("Command", typeof(ICommand), typeof(viewLearnSpeak), new PropertyMetadata(null));

    public ICommand Command
    {
        get => (ICommand)GetValue(CommandProperty);
        set => SetValue(CommandProperty, value);
    }

    #endregion
}

This Usercontrol is now used inside a Window inside a DataTemplate, and here is where the trouble starts:

<DataTemplate DataType="{x:Type vm:vmLearnSpeak}">
    <local:viewLearnSpeak Command="{Binding cmdVMNotKnown, ElementName=root}" />
</DataTemplate>

The Window (View) is bound to a ViewModel (vmSession), which should host the command code, so that for any CustomControl that will be used by the DataTemplate , the valid Customcontrol can bind its command to an action in the vmSession ViewModel (all CustomControls will do the same action when clicked on a particular button they host).

Command definition in code Behind:

#region Command: Not Known

private ICommand _cmdVMNotKnown;
public ICommand cmdVMNotKnown
{
    get
    {
        if (_cmdVMNotKnown == null)
        {
            _cmdVMNotKnown = new RelayCommand(
                param => this.doVMNotKnown(),
                param => { return true; }
            );
        }
        return _cmdVMNotKnown;
    }
}

protected void doVMNotKnown()
{
}

#endregion

Unfortunately I just can get the binding to work this way and I am running out of clues on how to bind the buttons command not to the Viewmodel behind the UserControl, but to the Viewmodel hosting the Usercontrols ViewModel in a MVVM manner without reyling on delegates or other handlers...

Here is the full window XAML (the ContentControl using the DataTemplate is at the end):

<Window x:Class         = "Vokabelizer.Views.wndSession"
    xmlns           = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x         = "http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d         = "http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc        = "http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local     = "clr-namespace:Vokabelizer.Views"
    xmlns:res       = "clr-namespace:Vokabelizer.Resources"
    xmlns:vm        = "clr-namespace:Vokabelizer.ViewModels"
    mc:Ignorable    = "d"
    Title           = "{x:Static res:Strings.wndLearnTitle}" 
    Height          = "410"
    Width           = "800"
    ResizeMode      = "NoResize"
    x:Name          = "root">

<Window.DataContext>
    <vm:vmSession />
</Window.DataContext>

<Window.Resources>
    <DataTemplate DataType="{x:Type vm:vmLearnSpeak}">
        <local:viewLearnSpeak Command="{Binding cmdVMNotKnown, ElementName=root}" />
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:vmLearnWrite}">
        <local:viewLearnWrite />
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:vmLearnListen}">
        <local:viewLearnListen />
    </DataTemplate>
</Window.Resources>

<Grid Margin="10">

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width = "Auto" />
        <ColumnDefinition Width = "*" />
        <ColumnDefinition Width = "Auto" />
    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>
        <RowDefinition Height = "50"  />
        <RowDefinition Height = "300" />
    </Grid.RowDefinitions>

    <!-- Title description -->
    <TextBlock x:Name="txtModeDescription"  Text="{Binding LearnTitle}"
               HorizontalAlignment="Left"   VerticalAlignment="Center"
               Grid.Column="0"              Grid.ColumnSpan="1" 
               Grid.Row="0"                 Grid.RowSpan="1"
               Margin="0, 0, 30, 0"        Foreground="DarkGray"
               FontWeight="Bold" FontSize="18" FontFamily="Arial Black" />

    <!-- Progress Slider -->
    <Slider x:Name="sliderProgress"
            HorizontalAlignment="Stretch"   VerticalAlignment="Center"
            Grid.Column="1"                 Grid.ColumnSpan="1" 
            Grid.Row="0"                    Grid.RowSpan="1"
            Minimum="0"                     Maximum="11" />

    <!-- Title status -->
    <TextBlock x:Name="txtBatchAdvancement" Text="{Binding LearnProgressBatch}"
               HorizontalAlignment  = "Right"   VerticalAlignment   = "Center"
               Grid.Column          = "3"       Grid.ColumnSpan     = "1" 
               Grid.Row             = "0"       Grid.RowSpan        = "1"
               Margin               = "10, 0, 0, 0"/>

    <Border Grid.Row            = "1"
            Grid.Column         = "0"
            HorizontalAlignment = "Center"
            VerticalAlignment   = "Center"
            Padding             = "0">

        <Image Source               = "Demo.png"
               Height               = "300"
               Width                = "300"
               HorizontalAlignment  = "Center"
               VerticalAlignment    = "Center" />

    </Border>

    <ContentControl Content     = "{Binding learningViewModel}"
                    Grid.Row    = "1"   Grid.RowSpan="1"
                    Grid.Column = "1"   Grid.ColumnSpan="2"
                    Height      = "300"
                    Margin      = "20, 0, 20, 0"/>

</Grid>

And here the from the Window ViewModel the part where the UserControl ViewModel is exposed for the DataTemplate:

private ILearnVM _learningViewModel;
/// <summary>
/// The Learning Provider View Model
/// </summary>
public ILearnVM learningViewModel 
{
    get => this._learningViewModel;
    private set
    {
        this._learningViewModel = value;

        this.OnPropertyChanged(nameof(this.learningViewModel));
    }
}

Well I must really thank @ΩmegaMan and other requesting some code to work.

I build a small Toy Project which really helped debug the issue.

What did I change? Actually it was quite simple, inside the DataTemplate, I just needed to use a relative source pointing to the Window and use a DataContext reference to the command property of the Window ViewModel (which is referenced in the Window Datacontext).

I ended up changing:

<Window.Resources>
    <DataTemplate DataType="{x:Type vm:vmLearnSpeak}">
        <local:viewLearnSpeak Command="{Binding cmdVMNotKnown, ElementName=root}" />
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:vmLearnWrite}">
        <local:viewLearnWrite />
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:vmLearnListen}">
        <local:viewLearnListen />
    </DataTemplate>
</Window.Resources>

To:

<Window.Resources>
    <DataTemplate DataType="{x:Type vm:vmLearnSpeak}">
        <local:viewLearnSpeak Command="{Binding DataContext.cmdVMNotKnown, RelativeSource={RelativeSource AncestorType=local:wndSession}}" />
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:vmLearnWrite}">
        <local:viewLearnWrite />
    </DataTemplate>
    <DataTemplate DataType="{x:Type vm:vmLearnListen}">
        <local:viewLearnListen />
    </DataTemplate>
</Window.Resources>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM