Search code examples
c#wpfeventscaliburn.micro

Capturing TextChanged event from DataGridComboBoxColumn using Caliburn Micro


I'm having problems passing the TextChanged event of the TextBox part of a ComboBox to the associated view model.

As expected:

  1. On TextChanged the OnTextChanged() method in the code behind is called.
  2. On KeyUp the OnKeyUp() method in the view model is called.

However:

  1. On TextChanged the OnTextChanged() method in the view model is not called.

Why is it not called and how can I fix it?

<UserControl x:Class="AutoComplete.Views.ShellView"
             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:cal="http://www.caliburnproject.org"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             >
    <UserControl.Resources>
        <FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
    </UserControl.Resources>
    <Grid>
        <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
        <DataGrid ItemsSource="{Binding Rows}" AutoGenerateColumns="False" CanUserAddRows="True" CanUserDeleteRows="True">
            <DataGrid.Columns>

                <DataGridComboBoxColumn Header="Code">
                    <DataGridComboBoxColumn.EditingElementStyle>
                        <Style TargetType="ComboBox">
                            <EventSetter Event="TextBoxBase.TextChanged" Handler="OnTextChanged"/>
                            <Setter Property="cal:Action.TargetWithoutContext" Value="{Binding DataContext, Source={StaticResource ProxyElement}}"/>
                            <Setter Property="cal:Message.Attach" Value="[Event TextBoxBase.TextChanged] = [Action OnTextChanged($source, $dataContext)]; [Event KeyUp] = [Action OnKeyUp()]"/>
                            <Setter Property="IsEditable" Value="True"/>
                            <Setter Property="ItemsSource" Value="{Binding DataContext.Suggestions, Source={StaticResource ProxyElement}}"/>
                        </Style>
                    </DataGridComboBoxColumn.EditingElementStyle>
                </DataGridComboBoxColumn>

            </DataGrid.Columns>
        </DataGrid>

    </Grid>
</UserControl>

Solution

  • This doesn't work because Caliburn Micro by default uses the EventTrigger class for Message.Attach. According to this post EventTrigger uses reflection to find the event using the EventName property which fails since the ComboBox does not expose an event called TextBoxBase.TextChanged. Neither does it expose a TextChanged event, it's the TextChanged bubbling up from the TextBox component of the ComboBox that is supposed to be caught.

    The above post also provides an adaptable solution. First a new RoutedEventTrigger class is created:

    public class RoutedEventTrigger : EventTriggerBase<DependencyObject>
    {
        public RoutedEvent RoutedEvent { get; set; }
    
        protected override void OnAttached()
        {
            var element = (AssociatedObject as Behavior as IAttachedObject)?.AssociatedObject as UIElement
                      ?? AssociatedObject as UIElement;
    
            element?.AddHandler(RoutedEvent, new RoutedEventHandler(OnRoutedEvent));
        }
    
        void OnRoutedEvent(object sender, RoutedEventArgs args)
        {
            OnEvent(args);
        }
    
        protected override string GetEventName()
        {
            return RoutedEvent.Name;
        }
    }
    

    Then Caliburn Micro is configured to use RoutedEventTrigger instead of EventTrigger if possible:

    public class Bootstrapper : BootstrapperBase
    {
        public Bootstrapper()
        {
            Initialize();
        }
    
        private RoutedEventTrigger CreateRoutedEventTrigger(DependencyObject target, string routedEvent)
        {
            var routedEvents = EventManager.GetRoutedEvents().ToDictionary(r => $"{r.OwnerType.Name}.{r.Name}");
            if (routedEvents.ContainsKey(routedEvent))
            {
                var trigger = new RoutedEventTrigger
                {
                    RoutedEvent = routedEvents[routedEvent]
                };
                trigger.Attach(target);
                return trigger;
            }
            return null;
        }
    
        protected override void OnStartup(object sender, StartupEventArgs args)
        {
            var baseCreateTrigger = Parser.CreateTrigger;
            Parser.CreateTrigger = (target, triggerText) =>
            {
                var baseTrigger = baseCreateTrigger(target, triggerText);
                var baseEventTrigger = baseTrigger as EventTrigger;
                return CreateRoutedEventTrigger(target, baseEventTrigger?.EventName ?? "") ?? baseTrigger;
            };
           ...
        }
        ...
    }
    

    After this setup bubbling RoutedEvents can be used.