wpfeventsmvvmicommand

Custom event in custom view model binding


I have searched far and wide. My problem is, that I can’t find a solution to this very simple problem of mine. I have a page, which hosts a data grid. The goal is to have the name in the data grid editable on double click. The data is retrieved from a database. I want to update the “Name” property only when the edit process is completed and not on “PropertyChanged”.

My custom control:

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Ey.Ventuz.SessionManager.Ui
{
    /// <summary>
    /// Interaktionslogik für TextEditInPlace.xaml
    /// </summary>
    public partial class TextEditInPlace : UserControl, INotifyPropertyChanged
    {
        #region INotify
        public event PropertyChangedEventHandler PropertyChanged;
        public event EventHandler<string> NameChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion
        private bool isEditing;
        /// <summary>
        /// Is true while editing
        /// </summary>
        public bool IsEditing
        {
            get { return isEditing; }
            set 
            { 
                isEditing = value;
                OnPropertyChanged(nameof(IsEditing));
                if(isEditing == false)
                {
                    NameChanged?.Invoke(this, EditableText);
                }
            }
        }

        public string EditableText
        {
            get { return (string)GetValue(EditableTextProperty); }
            set { SetValue(EditableTextProperty, value); }
        }

        // Using a DependencyProperty as the backing store for EditableText.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty EditableTextProperty =
            DependencyProperty.Register("EditableText", typeof(string), typeof(TextEditInPlace), new PropertyMetadata("test"));


        public TextEditInPlace()
        {
            InitializeComponent();
        }

        private void TextBlock_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ClickCount == 2)
            {
                IsEditing = true;
            }
                
        }

        private void TextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Return)
            {
                IsEditing = false;
            }
        }

        private void TextBox_LostFocus(object sender, RoutedEventArgs e)
        {
            IsEditing = false;
        }
    }
}

My Data Grid:

<DataGrid Name="myDataGrid" 
                      ItemsSource="{Binding ItemList, UpdateSourceTrigger=PropertyChanged}" 
                      SelectedItem="{Binding SelectedStyle}" 
                      RowHeaderWidth="100" 
                      AutoGenerateColumns="False" 
                      HorizontalAlignment="Stretch"
                      HeadersVisibility="Column" 
                      SelectionMode="Single"
                      VerticalScrollBarVisibility="Auto"
                      HorizontalScrollBarVisibility="Disabled">

                    <DataGrid.Columns>
                        <DataGridTemplateColumn Header="Thumbnail"  Width="120">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <Image Source="{Binding Path=Logo}"
                                           Width="100"
                                           Stretch="Uniform"
                                           HorizontalAlignment="Left"
                                           Margin="3"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                        <DataGridTemplateColumn Header="Name"  Width="120">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <local:TextEditInPlace EditableText="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                                        <i:Interaction.Triggers>
                                            <i:EventTrigger EventName="NameChanged">
                                                <i:InvokeCommandAction Command="{Binding UpdateListItemNameCommand}" />
                                            </i:EventTrigger>
                                        </i:Interaction.Triggers>
                                    </local:TextEditInPlace>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                        <DataGridTextColumn Binding="{Binding Path=Created, StringFormat=\{0:dd.MM.yyyy \}}" Header="Date"  Width="*"/>
                        <DataGridTextColumn Binding="{Binding Path=CreatedBy}" Header="Author"  Width="*"/>
                    </DataGrid.Columns>
                </DataGrid>

My ViewModel:

using Ey.Ventuz.SessionManager.Data;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Data;
using System.Windows.Input;

namespace Ey.Ventuz.SessionManager.Ui
{
    public class StyleConfigurationViewModel : BaseViewModel
    {
        public List<VentuzStyle> ventuzStyles;
        public ICollectionView ItemList { get; set; }

        public DefaultStyleData StyleData { get; set; }

        private VentuzStyle selectedStyle;
        public VentuzStyle SelectedStyle
        {
            get { return selectedStyle; }
            set
            {
                if (value != null)
                {
                    selectedStyle = value;
                }
            }
        }

        public ICommand UpdateListItemNameCommand { get; set; }
        public ICommand GoBackCommand { get; set; }
        public ICommand DuplicateStyleCommand { get; set; }
        public ICommand RemoveStyleCommand { get; set; }
        public StyleConfigurationViewModel()
        {
            InitializeProperties();
            FillList();
            GoBackCommand = new RelayCommand(() => GoBack());
            DuplicateStyleCommand = new RelayCommand(() => DuplicateStyle());
            RemoveStyleCommand = new RelayCommand(() => RemoveStyle());
            UpdateListItemNameCommand = new RelayCommand(() => UpdateListItemName());
        }

        private void UpdateListItemName()
        {
            
        }

        private void InitializeProperties()
        {
            ventuzStyles = new List<VentuzStyle>(SessionSelectionData.GetVentuzStyles());

            if (ventuzStyles != null && ventuzStyles.Count > 0)
            {
                try
                {
                    foreach (VentuzStyle ventuzStyle in ventuzStyles)
                    {
                        ventuzStyle.PropertyChanged += VentuzStyle_PropertyChanged;
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
            }
        }

        private void VentuzStyle_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
           
        }

        private void FillList()
        {
            ItemList = new CollectionViewSource() { Source = ventuzStyles }.View;
        }

        private void GoBack()
        {
            AggregatorLight.GoBack("go back");
        }
        private void DuplicateStyle()
        {
            VentuzStyle _ventuzStyle = new VentuzStyle(); 
            _ventuzStyle = ObjectCopier.DeepCopy(SelectedStyle);
            ventuzStyles.Add(SessionSelectionData.CreateStyle(_ventuzStyle));
            ItemList.Refresh();
        }
        private void RemoveStyle()
        {
            if(ventuzStyles.Count() > 0)
            {
                SessionSelectionData.RemoveStyle(SelectedStyle);
                ventuzStyles.Remove(SelectedStyle);
            }
            ItemList.Refresh();
        }
    }
}

How do I create a custom event in a custom user control? How do I consume it in XAML? I am greatful for any comments.

thanks a lot

Update: this is the Xaml for the TextEditInPlace:

<Grid Height="Auto" Width="Auto">
    <TextBlock Text="{Binding EditableText, ElementName=userControl}"
               Visibility="{Binding IsEditing, ElementName=userControl, Converter={local:BooleanToInvisibilityConverter}}" 
               MouseLeftButtonDown="TextBlock_MouseLeftButtonDown" />
    <TextBox Text="{Binding EditableText, ElementName=userControl, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
             Visibility="{Binding IsEditing, ElementName=userControl, Converter={local:BooleanToVisibilityConverter}}"
             Style="{StaticResource TextEditBox}" Margin="-4"
             KeyDown="TextBox_KeyDown"
             LostFocus="TextBox_LostFocus" />
</Grid>

Solution

  • First of all, there is no need to implement INotifyPropertyChanged on a Control or DependencyObject in general. It is recommended to implement all properties that serve as a binding target as dependency properties (as you did). Dependency properties provide a better performance.

    Second the postulated goal " to have the name in the data grid editable on double click" is the default behavior of a DataGrid text cell. The TextEditInPlace custom control is absolutely redundant. It only adds complexity but no additional benefit.

    In general a column consists of two templates: a CellTemplate and a CellEditingTemplate. When the cell is transitioned into edit mode e.g. by double clicking the cell, then the CellEditingTemplate is loaded. The data during the editing is only committed when the user exits the edit mode. This is the moment where the data is sent back to the binding source. Therefore it is not necessary for the edited control to track the state of the cell or tocreate a custom state. Since the CellTemplate purpose is to display the value, a simple TextBlock is sufficient as content host.

    Also the Bindiiung.UpdateSourceTrigger is set to UpdateSourceTrigger.PropertyChanged by default, except when the Binding.Target explicitly specified the trigger differently e.g., TextBox.Text or Selector.SelectedItem, which are TwoWay by default.
    You can therefore leave the assignment of the default trigger and shorten your binding expressions in favor of readability.

    If you still want to use your custom text edit control, your templates should look as followed. Note that in order to let your control execute commands, simply implement ICommandSource:

    TextEditInPlace.xaml.cs

    public partial class TextEditInPlace : UserControl, ICommandSource
    {
        public static readonly DependencyProperty EditableTextProperty = DependencyProperty.Register(
          "EditableText",
          typeof(ICommand),
          typeof(TextEditInPlace),
          new FrameworkPropertyMetadata(
            "test", 
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
            OnEditTextChanged));
          new PropertyMetadata("test", OnEditableTextChanged));
    
        public string EditableText
        {
            get { return (string)GetValue(EditableTextProperty); }
            set { SetValue(EditableTextProperty, value); }
        }
    
        #region Implementation of ICommandSource
    
        public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
          "Command",
          typeof(ICommand),
          typeof(TextEditInPlace),
          new PropertyMetadata(default(ICommand)));
    
        public ICommand Command
        {
          get => (ICommand)GetValue(SectionNavigator.CommandProperty);
          set => SetValue(SectionNavigator.CommandProperty, value);
        }
    
        public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(
          "CommandParameter",
          typeof(object),
          typeof(TextEditInPlace),
          new PropertyMetadata(default(object)));
    
        public object CommandParameter
        {
          get => (object)GetValue(SectionNavigator.CommandParameterProperty);
          set => SetValue(SectionNavigator.CommandParameterProperty, value);
        }
    
        public static readonly DependencyProperty CommandTargetProperty = DependencyProperty.Register(
          "CommandTarget",
          typeof(IInputElement),
          typeof(TextEditInPlace),
          new PropertyMetadata(default(IInputElement)));
    
        public IInputElement CommandTarget
        {
          get => (IInputElement)GetValue(SectionNavigator.CommandTargetProperty);
          set => SetValue(SectionNavigator.CommandTargetProperty, value);
        }
    
       #endregion
    
    
        public TextEditInPlace()
        {
             InitializeComponent();
        }
    
        private static void OnEditTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var _this = d as TextEditInPlace;
            if (e.OldValue != null
              && e.NewValue != e.OldValue
              && _this.Command?.CanExecute(_this.CommandParameter) ?? false)
            {
                _this.Command?.Execute(_this.CommandParameter);
            }        
        }
    }
    

    DataGrid column template

    <DataGridTemplateColumn Header="Name"  Width="120">
      <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
          <TextBlock Text="{Binding Name}" />
        </DataTemplate>
      </DataGridTemplateColumn.CellTemplate>
    
      <DataGridTemplateColumn.CellEditTemplate>
        <DataTemplate>
          <local:TextEditInPlace EditableText="{Binding Name}" 
                                 Command="{Binding UpdateListItemNameCommand}" />
        </DataTemplate>
      </DataGridTemplateColumn.CellEditTemplate>
    </DataGridTemplateColumn>