Search code examples
c#wpfmvvmcustom-controlsinputbinding

Update Properties on Custom Control click


I'm working with WPF (MVVM pattern in particular) and I'm trying to create a simple application that shows a list of tasks. I created a custom control called TaskListControl that shows a list of other custom controls named TaskListItemControl and each of them has its own ViewModel.

Here is the TaskListItemControl template, where you can see the InputBindings and the Triggers that affects the control appearence when the IsSelected is set to true:

<UserControl x:Class="CSB.Tasks.TaskListItemControl"
         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:CSB.Tasks"
         mc:Ignorable="d"
         d:DesignHeight="70" 
         d:DesignWidth="400">

<!-- Custom control that represents a Task. -->
<UserControl.Resources>
    <!-- The control style. -->
    <Style x:Key="ContentStyle" TargetType="{x:Type ContentControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ContentControl}">

                    <Border x:Name="ContainerBorder" BorderBrush="{StaticResource LightVoidnessBrush}"
                            Background="{StaticResource VoidnessBrush}"
                            BorderThickness="1"
                            Margin="2">

                        <Border.InputBindings>
                            <MouseBinding MouseAction="LeftClick" Command="{Binding SelctTaskCommand}"/>
                        </Border.InputBindings>

                        <!-- The grid that contains the control. -->
                        <Grid Name="ContainerGrid" Background="Transparent">

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

                            <!-- Border representing the priority state of the Task:
                            The color is defined by a ValueConverter according to the PriorityLevel of the Task object. -->
                            <Border Grid.Column="0"
                                    Width="10"
                                    Background="{Binding Priority, Converter={local:PriorityLevelToRGBConverter}}">
                            </Border>

                            <!-- Border containing the Task's informations. -->
                            <Border Grid.Column="1" Padding="5">
                                <StackPanel>
                                    <!-- The title of the Task. -->
                                    <TextBlock Text="{Binding Title}" FontSize="{StaticResource TaskListItemTitleFontSize}" Foreground="{StaticResource DirtyWhiteBrush}"/>

                                    <!-- The customer the Taks refers to. -->
                                    <TextBlock Text="{Binding Customer}" Style="{StaticResource TaskListItemControlCustomerTextBlockStyle}"/>

                                    <!-- The description of the Task. -->
                                    <TextBlock Text="{Binding Description}"
                                               TextTrimming="WordEllipsis"
                                               Foreground="{StaticResource DirtyWhiteBrush}"/>
                                </StackPanel>
                            </Border>

                            <!-- Border that contains the controls for the Task management. -->
                            <Border Grid.Column="2"
                                    Padding="5">

                                <!-- Selection checkbox of the Task. -->
                                <CheckBox Grid.Column="2" VerticalAlignment="Center"/>
                            </Border>

                        </Grid>

                    </Border>

                    <!-- Template triggers. -->
                    <ControlTemplate.Triggers>

                        <DataTrigger Binding="{Binding IsSelected}" Value="True">
                            <Setter Property="Background" TargetName="ContainerBorder" Value="{StaticResource OverlayVoidnessBrush}"/>
                            <Setter Property="BorderBrush" TargetName="ContainerBorder" Value="{StaticResource PeterriverBrush}"/>
                        </DataTrigger>

                        <EventTrigger RoutedEvent="MouseEnter">
                            <BeginStoryboard>
                                <Storyboard>
                                    <ColorAnimation Duration="0:0:0:0" To="{StaticResource OverlayVoidness}" Storyboard.TargetName="ContainerGrid" Storyboard.TargetProperty="Background.Color"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>

                        <EventTrigger RoutedEvent="MouseLeave">
                            <BeginStoryboard>
                                <Storyboard>
                                    <ColorAnimation Duration="0:0:0:0" To="Transparent" Storyboard.TargetName="ContainerGrid" Storyboard.TargetProperty="Background.Color"/>
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger>
                    </ControlTemplate.Triggers>

                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</UserControl.Resources>

<!-- Content of the control: assignment of the DataContext for design-time testing. -->
<ContentControl d:DataContext="{x:Static local:TaskListItemDesignModel.Instance}" 
                Style="{StaticResource ContentStyle}"/>

And here is the TaskListItemViewModel where the Action should be executed (all the PropertyChanged boilerplate code is handled inside the BaseViewModel class):

/// <summary>
/// The ViewModel for the <see cref="TaskListItemControl"/>.
/// </summary>
public class TaskListItemViewModel : BaseViewModel
{
    #region Public Properties

    /// <summary>
    /// Priority level of the task.
    /// </summary>
    public PriorityLevel Priority { get; set; }

    /// <summary>
    /// The name of the task.
    /// </summary>
    public string Title { get; set; }

    /// <summary>
    /// The customer the task refers to.
    /// </summary>
    public string Customer { get; set; }

    /// <summary>
    /// The description of the task.
    /// </summary>
    public string Description { get; set; }

    /// <summary>
    /// True if the Task is the selected one in the task list.
    /// </summary>
    public bool IsSelected { get; set; }

    #endregion

    #region Commands

    /// <summary>
    /// The command fired whenever a task is selected.
    /// </summary>
    public ICommand SelectTaskCommand { get; set; }

    #endregion

    #region Constructor

    /// <summary>
    /// The <see cref="TaskListItemViewModel"/> default constructor.
    /// </summary>
    public TaskListItemViewModel()
    {
        // Initialize commands.
        // When the task is selected, IsSelected becomes true.
        // The command will do other stuff in the future.
        SelectTaskCommand = new RelayCommand(() => IsSelected = true);
    }

    #endregion
}

The data is provided through a design model bound to the TaskListControl control where the properties of each item of the list are hardcoded (this design model will be replaced with a database since this class just provides dummy data):

/// <summary>
/// The <see cref="TaskListControl"/> design model that provides dummy data for the XAML testing.
/// </summary>
public class TaskListDesignModel : TaskListViewModel
{
    #region Public Properties

    /// <summary>
    /// A single instance of the <see cref="TaskListDesignModel"/> class.
    /// </summary>
    public static TaskListDesignModel Instance => new TaskListDesignModel();

    #endregion

    #region Constructor

    /// <summary>
    /// The <see cref="TaskListDesignModel"/> default constructor that provides dummy data.
    /// </summary>
    public TaskListDesignModel()
    {
        Items = new ObservableCollection<TaskListItemViewModel>
        {
            new TaskListItemViewModel
            {
                Title = "Activity #1",
                Customer = "Internal",
                Description = "This is activity #1.",
                Priority = PriorityLevel.High,
                IsSelected = false
            },

            new TaskListItemViewModel
            {
                Title = "Activity #2",
                Customer = "Internal",
                Description = "This is activity #2.",
                Priority = PriorityLevel.High,
                IsSelected = false
            },

            new TaskListItemViewModel
            {
                Title = "Activity #3",
                Customer = "Internal",
                Description = "This is activity #3.",
                Priority = PriorityLevel.High,
                IsSelected = false
            },

            new TaskListItemViewModel
            {
                Title = "Activity #4",
                Customer = "Internal",
                Description = "This is activity #4.",
                Priority = PriorityLevel.Medium,
                IsSelected = false
            },

            new TaskListItemViewModel
            {
                Title = "Activity #5",
                Customer = "Internal",
                Description = "This is activity #5.",
                Priority = PriorityLevel.Medium,
                IsSelected = false
            },

            new TaskListItemViewModel
            {
                Title = "Activity #6",
                Customer = "Internal",
                Description = "This is activity #6.",
                Priority = PriorityLevel.Low,
                IsSelected = false
            }

        };
    }

    #endregion
}

Here is the result: items_list

What I want to do when the list item is selected is to update his IsSelected property in the ViewModel and change its appearence through Triggers, but nothing happens when I click on an item.

Here is the link to the GitHub repository of the entire project if needed.

What am I missing? Thank you in advance for the help.


Solution

  • You should fix this two problems to solve the selection issue:

    1) I spotted a typo inside your TaskListItemControl: Line 25 should be "SelectTaskCommand" on the Command binding. This will finally invoke the command.

    2) Then in your TaskListItemViewModel you have to make your properties raise the PropertyChanged event. I highly recommend this for all your ViewModel properties. But to solve your problem this must apply at least to the IsSelected property:

    private bool isSelected;
    public bool IsSelected 
    { 
      get => this.isSelected; 
      set 
      { 
        if (value.Equals(this.isSelected)
        {
          return;
        }
    
        this.isSelected = value;  
        OnPropertyChanged(); 
      }
    }
    

    This will make any changes to IsSelected propagate and thus the DataTrigger can work as expected.

    And just another recommendation is to modify the PropertyChanged invocator signature in BaseViewModel to:

    public void OnPropertyChanged([CallerMemberName] string propertyName = null)
    

    This way you avoid to always pass the property name as an argument.

    If you want the CheckBox to reflect the selection state and should be used to undo the selection, just add a TwoWay binding to its IsChecked property:

    <CheckBox IsChecked="{Binding IsSelected, Mode=TwoWay}" />