Search code examples
c#wpfwpf-controls

WPF - Issues with passing commands from viewmodel to usercontrols


I am trying to create a user control that will replace the header of a group box where there is buttons/actions shown on the far right corner of the group box header (see image below)

enter image description here

To do this I have created two user controls...

  1. a GroupBoxHeaderButton control which holds the command and command parameter as a dependency property

     <UserControl.Resources>
         <converters:ImageNameToImageSourceConverter x:Key="ImageNameToImageSourceConverter"/>
     </UserControl.Resources>
     <Border HorizontalAlignment="Right" Style="{StaticResource GroupBoxHeaderButton}" Margin="0 -4 0 -4" Width="27" Height="24">
         <Viewbox Margin="5">
             <accentImage:AccentImage Color="White" Source="{Binding ElementName=HeaderButtonUserControl, Path=ImageName, Converter={StaticResource ImageNameToImageSourceConverter}}" Width="15" Height="15"/>
         </Viewbox>
    
         <b:Interaction.Triggers>
             <b:EventTrigger EventName="MouseUp">
                 <b:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=local:GroupboxHeaderButton},Path=Command}" CommandParameter="{Binding  RelativeSource={RelativeSource AncestorType=local:GroupboxHeaderButton}, Path=CommandParameter}"/>
             </b:EventTrigger>
         </b:Interaction.Triggers>
     </Border>
    
     public static readonly DependencyProperty ImageNameProperty = DependencyProperty.Register(
         nameof(ImageName), typeof(string), typeof(GroupboxHeaderButton), new FrameworkPropertyMetadata(default(string)));
    
     public string ImageName
     {
         get => (string)GetValue(ImageNameProperty);
         set => SetValue(ImageNameProperty, value);
     }
    
     public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
         nameof(Command), typeof(ICommand), typeof(GroupboxHeaderButton),  new UIPropertyMetadata(null));
    
     public ICommand Command
     {
         get => (ICommand)GetValue(CommandProperty);
         set => SetValue(CommandProperty, value);
     }
    
     public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(
         nameof(CommandParameter), typeof(object), typeof(GroupboxHeaderButton),  new UIPropertyMetadata(null));
    
     public object CommandParameter
     {
         get => (object)GetValue(CommandParameterProperty);
         set => SetValue(CommandParameterProperty, value);
     }
    
  2. I have a GroupBoxHeader user control which handles the groupbox header text and placement of the groupbox buttons

     <DockPanel LastChildFill="True" Margin="0 0 -4 0">
         <TextBlock Foreground="{StaticResource MahApps.Brushes.IdealForeground}" Text="{Binding HeaderText, ElementName=HeaderUserControl}"/>
         <ItemsControl ItemsSource="{Binding ElementName=HeaderUserControl, Path=InnerContent, Mode=OneWay}">
             <ItemsControl.ItemsPanel>
                 <ItemsPanelTemplate>
                     <StackPanel HorizontalAlignment="Right" Orientation="Horizontal"/>
                 </ItemsPanelTemplate>
             </ItemsControl.ItemsPanel>
    
         </ItemsControl>
     </DockPanel>
    

    [ContentProperty("InnerContent")]

    public partial class GroupboxHeader : UserControl {

    public ObservableCollection InnerContent { get; set; } = new ObservableCollection();

     public static readonly DependencyProperty HeaderTextProperty = DependencyProperty.Register(
         nameof(HeaderText), typeof(string), typeof(GroupboxHeader), new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.AffectsRender));
     public string HeaderText
     {
         get => (string)GetValue(HeaderTextProperty);
         set => SetValue(HeaderTextProperty, value);
     }
    
     public GroupboxHeader()
     {
         InitializeComponent();
     }
    

    }

I have also made some helper user controls for grouping common actions.. For example the plus and minus. (I have removed the name spaces from the xaml)

<UserControl x:Name="AddRemoveHeaderUserControl">
    <groupboxHeaderControls:GroupboxHeader HeaderText="{Binding HeaderText, ElementName=AddRemoveHeaderUserControl}">
        <groupboxHeaderControls:GroupboxHeaderButton ToolTip="{Binding Path=RemoveTooltip, RelativeSource={RelativeSource AncestorType=local:AddRemoveHeader}}" Command="{Binding Path=RemoveCommand, RelativeSource={RelativeSource AncestorType=local:AddRemoveHeader}}" CommandParameter="{Binding Path=RemoveCommandParameter, RelativeSource={RelativeSource AncestorType=local:AddRemoveHeader}}" ImageName="minus_64x64"/>
        <groupboxHeaderControls:GroupboxHeaderButton ToolTip="{Binding Path=AddTooltip, RelativeSource={RelativeSource AncestorType=local:AddRemoveHeader}}" Command="{Binding Path=AddCommand, RelativeSource={RelativeSource AncestorType=local:AddRemoveHeader}}" CommandParameter="{Binding Path=AddCommandParameter, RelativeSource={RelativeSource AncestorType=local:AddRemoveHeader}}" ImageName="plus_64x64"/>

    </groupboxHeaderControls:GroupboxHeader>
</UserControl>

The Problem

I am having a hell of a time with the binding of commands and commandparameters.. Sometimes it works, sometimes it doesn't, depending on how the usercontrol is used in a particular view.

For example, I have a View that appears when a certain option is enabled within our application (the same image above).
If I use the usercontrol which groups the common actions (AddRemoveHeader), the commands do not trigger at all if the view is not open at loadtime... if the view is open at load time, then the commands trigger, but the command parameter is still passing null .

If I specify the GroupBoxHeader and buttons directly, the commands trigger, but the commandparameter is not working properly.

For some reason the commandparameter is passing null in this case, however, it works if I dont use my usercontrol and explicity specify xaml and bind everything directly within the groupbox.header

To add to the confusion, these user controls work fine in other areas of our application.. both commands and the commandparameters.. I imagine I am doing something wrong with the binding within the usercontrols, so any help would be greatly appreciated.

Note I am referring to the commandparameter of the Remove button, the Add button works fine since it does not rely on a command parameter

<GroupBox>
            <GroupBox.Header>
                <!-- Commands dont Work For This if the view is not active on window load, otherwise commands trigger but parameter still null  -->
                <!--<addRemoveHeader:AddRemoveHeader HeaderText="{Binding CircularClassificationProfiles.Count, StringFormat='Circular Profiles: {0}'}" AddTooltip="Add New Circular Profile" AddCommand="{Binding CreateCircularProfileCommand}" RemoveTooltip="Remove Selected Profile" RemoveCommand="{Binding RemoveCircularProfilesCommand}" RemoveCommandParameter="{Binding ElementName=DataGrid, Path=SelectedItems}"/>  -->

                <!-- Command Parameter doesnt work For This  -->
                <groupboxHeaderControls:GroupboxHeader HeaderText="{Binding CircularClassificationProfiles.Count, StringFormat='Circular Profiles: {0}'}">
                    <groupboxHeaderControls:GroupboxHeaderButton ToolTip="Remove Selected" Command="{Binding Path=RemoveCircularProfilesCommand}" CommandParameter="{Binding ElementName=CircularDataGrid, Path=SelectedItems}" ImageName="minus_64x64"/>
                    <groupboxHeaderControls:GroupboxHeaderButton ToolTip="Add New Circular Profile" Command="{Binding Path=CreateCircularProfileCommand}" ImageName="plus_64x64"/>
                </groupboxHeaderControls:GroupboxHeader>

                <!-- This works fine  -->
                <!--<DockPanel LastChildFill="True">
                    <Label Foreground="{StaticResource MahApps.Brushes.IdealForeground}" ContentStringFormat="Circular Profiles: {0}" Content="{Binding CircularClassificationProfiles.Count}"/>
                    <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
                        <Border HorizontalAlignment="Right" Style="{StaticResource GroupBoxHeaderButton}" ToolTip="Remove Selected Profile" Width="32" Margin="0 -4 0 -4">
                            <TextBlock FontSize="20" Text="-" FontWeight="Bold" Margin="10 -10 10 -4" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="{StaticResource MahApps.Brushes.IdealForeground}"/>
                            <b:Interaction.Triggers>
                                <b:EventTrigger EventName="MouseUp">
                                    <b:InvokeCommandAction Command="{Binding RemoveCircularProfilesCommand}" CommandParameter="{Binding ElementName=DataGrid, Path=SelectedItems}"/>
                                </b:EventTrigger>
                            </b:Interaction.Triggers>
                        </Border>

                        <Border HorizontalAlignment="Right" Style="{StaticResource GroupBoxHeaderButton}" ToolTip="Add New Circular Profile" Width="32" Margin="0 -4 -4 -4">
                            <TextBlock FontSize="20" Text="+" FontWeight="Bold" Margin="10 -10 10 -5" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="{StaticResource MahApps.Brushes.IdealForeground}"/>
                            <b:Interaction.Triggers>
                                <b:EventTrigger EventName="MouseUp">
                                    <b:InvokeCommandAction Command="{Binding CreateCircularProfileCommand}" />
                                </b:EventTrigger>
                            </b:Interaction.Triggers>
                        </Border>
                    </StackPanel>
                </DockPanel>-->
            </GroupBox.Header>

            <DataGrid AutoGenerateColumns="False" x:Name="CircularDataGrid" ItemsSource="{Binding CircularClassificationProfiles}" dd:DragDrop.IsDragSource="True" dd:DragDrop.IsDropTarget="True" dd:DragDrop.UseDefaultDragAdorner="True"  attachedProperties:DataGridSelectionBinding.SelectedItems="{Binding SelectedCircularClassificationProfiles}">
                <DataGrid.Columns>
                    <DataGridTemplateColumn Header="" Width="20" IsReadOnly="False"/>

                    <DataGridTemplateColumn Header="Radius" Width="90">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <numericalTextbox:NumericalTextbox Value="{Binding Radius, UpdateSourceTrigger=PropertyChanged}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>

                    <DataGridTemplateColumn Header="Classification" Width="*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <combobox:ClassificationComboBox SelectedClassification="{Binding Classification, UpdateSourceTrigger=PropertyChanged}"  SelectionChanged="ClassificationComboBox_OnSelectionChanged"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>
        </GroupBox>

With the above xaml, I get an error thrown in the immediate window

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=CircularDataGrid'. BindingExpression:Path=SelectedItems; DataItem=null; target element is 'GroupboxHeaderButton' (Name='HeaderButtonUserControl'); target property is 'CommandParameter' (type 'Object')

When using the AddRemoveHeader user control instead and the view is not displayed on load, I get these errors

System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='Client.Common.User_Controls.Groupbox_Header_Controls.Add_Remove_Header.AddRemoveHeader', AncestorLevel='1''. BindingExpression:Path=RemoveTooltip; DataItem=null; target element is 'GroupboxHeaderButton' (Name='HeaderButtonUserControl'); target property is 'ToolTip' (type 'Object') System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='Client.Common.User_Controls.Groupbox_Header_Controls.Add_Remove_Header.AddRemoveHeader', AncestorLevel='1''. BindingExpression:Path=RemoveCommand; DataItem=null; target element is 'GroupboxHeaderButton' (Name='HeaderButtonUserControl'); target property is 'Command' (type 'ICommand') System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='Client.Common.User_Controls.Groupbox_Header_Controls.Add_Remove_Header.AddRemoveHeader', AncestorLevel='1''. BindingExpression:Path=RemoveCommandParameter; DataItem=null; target element is 'GroupboxHeaderButton' (Name='HeaderButtonUserControl'); target property is 'CommandParameter' (type 'Object') System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='Client.Common.User_Controls.Groupbox_Header_Controls.Add_Remove_Header.AddRemoveHeader', AncestorLevel='1''. BindingExpression:Path=AddTooltip; DataItem=null; target element is 'GroupboxHeaderButton' (Name='HeaderButtonUserControl'); target property is 'ToolTip' (type 'Object') System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='Client.Common.User_Controls.Groupbox_Header_Controls.Add_Remove_Header.AddRemoveHeader', AncestorLevel='1''. BindingExpression:Path=AddCommand; DataItem=null; target element is 'GroupboxHeaderButton' (Name='HeaderButtonUserControl'); target property is 'Command' (type 'ICommand') System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='Client.Common.User_Controls.Groupbox_Header_Controls.Add_Remove_Header.AddRemoveHeader', AncestorLevel='1''. BindingExpression:Path=AddCommandParameter; DataItem=null; target element is 'GroupboxHeaderButton' (Name='HeaderButtonUserControl'); target property is 'CommandParameter' (type 'Object') System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=DataGrid'. BindingExpression:Path=SelectedItems; DataItem=null; target element is 'AddRemoveHeader' (Name='AddRemoveHeaderUserControl'); target property is 'RemoveCommandParameter' (type 'Object')


Solution

  • You cannot use ElementName to bind to an element that resides in a different XAML namescope.

    Since the GroupboxHeader control renders the GroupboxHeaderButton elements in an ItemsControl, they are not in the same namescope as the DataGrid that you are trying to bind to.

    You should be able to workaround this by binding to the DataGrid through the parent GroupBox using a RelativeSource:

    <groupboxHeaderControls:GroupboxHeaderButton ...
        CommandParameter="{Binding Content.SelectedItems, RelativeSource={RelativeSource AncestorType=GroupBox}}" />