xamarin.formsmvvmdatatemplaterelativesourceeventtocommand

How to call Command from DataTemplate Entry TextChanged Event in Xamarin Forms?


Using DataTemplates in a CollectionView... I can call a ViewModel's Command from a button like this:

<Button Text="Test"
    Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}},
                      Path=BindingContext.TestCommand}"/>

Or from a gesture like this:

<Frame.GestureRecognizers>
    <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.TestCommand}"/>        
</Frame.GestureRecognizers>

So, why can't I call that command from an Entry's TextChanged Event like this?

<Entry x:Name="PortionEntry"
    Text ="{Binding QtyTest, Mode=TwoWay}">
    <Entry.Behaviors>
        <behavors:EventToCommandBehavior
            EventName="TextChanged"
            Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}},
                              Path=BindingContext.TestCommand}"/>
    </Entry.Behaviors>  

The code for EventToCommandBehavior works when not used in a DataTemplate

Here is a project illustrating the issue: https://github.com/BullCityCabinets/DataTemplateEventIssue

I got the button code form these fine folks: https://www.syncfusion.com/kb/11029/how-to-bind-command-from-viewmodel-to-external-itemtemplate-of-xamarin-forms-listview

Thanks!


Solution

  • I've looked at your example code and you appear to be using the Xamarin Forms Example code to implement your EventToCommandBehavior. This is also implemented in the Xamarin Community Toolkit in roughly the same manner. Note that these implementations inherit from Xamarin.Forms.Behavior.

    I also tried these examples to perform a Relative Source binding in a DataTemplate assigned to an ItemsView but when I would run the example (same as yours above) I would receive an InvalidOperationException at:

    Xamarin.Forms.Binding.ApplyRelativeSourceBinding (Xamarin.Forms.BindableObject targetObject, Xamarin.Forms.BindableProperty targetProperty) [0x0006c] in C:\Advanced Dev\Xamarin.Forms\Xamarin.Forms.Core\Binding.cs:158

    Heading over to the Xamarin source code you can see that the throw is a result of the binding targetObject not inheriting from Xamarin.Forms.Element when applying the binding in Binding.ApplyRelativeSourceBinding(). Since EventToCommandBehavior inherits from Xamarin.Forms.Behavior this is the result.

    The Xamarin Relative Binding docs don't specifically mention that the Binding Target requirement, they concern themselves with the binding Source obviously. But they do mention that these bindings search the Visual Tree or are relative to the Element:

    FindAncestor indicates the ancestor in the visual tree of the bound element.

    Self indicates the element on which the binding is being set,

    Since a Behavior isn't an Element and isn't part of the Visual Tree (It's stored in the VisualElement.Behaviors property), the binding doesn't have direct access to either to perform it's "search" during runtime and thus the binding can't ever be satisfied.

    I solved this by extending Entry and adding commands where needed. It's not the most reusable solution since I have to do it on other Elements like Switch but it works.

    public class Entry : Xamarin.Forms.Entry
    {
        public Entry()
        {
            this.TextChanged += this.OnTextChanged;
        }
    
        public static readonly BindableProperty TextChangedCommandProperty =
            BindableProperty.Create( nameof( Entry.TextChangedCommand ), typeof( ICommand ), typeof( Entry ) );
    
        public static readonly BindableProperty TextChangedCommandParameterProperty =
            BindableProperty.Create( nameof( Entry.TextChangedCommandParameter ), typeof( object ), typeof( Entry ) );
    
        public ICommand TextChangedCommand
        {
            get => (ICommand)this.GetValue( Entry.TextChangedCommandProperty );
            set => this.SetValue( Entry.TextChangedCommandProperty, (object)value );
        }
    
        public object TextChangedCommandParameter
        {
            get => this.GetValue( Entry.TextChangedCommandParameterProperty );
            set => this.SetValue( Entry.TextChangedCommandParameterProperty, value );
        }
    
        private void OnTextChanged( object sender, TextChangedEventArgs e )
        {
            if ( this.TextChangedCommand == null ||
                 !this.TextChangedCommand.CanExecute( this.TextChangedCommandParameter ) )
                return;
    
            this.TextChangedCommand.Execute( this.TextChangedCommandParameter );
        }
    }
    

    And the xaml embedded inside a DataTemplate:

        <my:Entry Grid.Column="1"
               Text="{Binding Value}"
               HorizontalTextAlignment="Start"
               HorizontalOptions="FillAndExpand"
               VerticalOptions="Center"
               VerticalTextAlignment="Center"
               Keyboard='Text'
               ClearButtonVisibility="WhileEditing"
               TextChangedCommand="{Binding BindingContext.TextChangedCommand, Mode=OneTime, Source={RelativeSource FindAncestor, AncestorType={x:Type ItemsView}}}"
               TextChangedCommandParameter="{Binding Mode=OneTime}" >
        </my:Entry>
    

    Update: After a few days of experimentation I've found another possible pattern to support this in a more generic fashion. I'll leave it up to the reader to decide on it's merits. I'm currently inclined to think it's a reasonable paradigm and is generic so there's no need to extend a bunch of existing Visual Elements.

    The code presented below models the idea of an Observer that holds the command and observes an event of it's child. The parent/observer extends the bland Xamarin.Forms.ContentView element with the Command/CommandParameter/Converter that we see implemented in the Xamarin Forms Example code and combines that with an Attached Property implementation for the EventName.

    The ContentView.Content property holds a single Xamarin.Forms.View object so there's no confusion with respect to the target of the Attached property. Event Handlers are all static so there shouldn't be any leaking issues.

    public class EventToCommandObserver : ContentView
    {
        public static readonly BindableProperty EventNameProperty = BindableProperty.CreateAttached( "EventName",
            typeof( string ), typeof( View ), null, propertyChanged: OnEventNameChanged );
    
        public static readonly BindableProperty CommandProperty =
            BindableProperty.Create( nameof( Command ), typeof( ICommand ), typeof( EventToCommandObserver ) );
    
        public static readonly BindableProperty CommandParameterProperty =
            BindableProperty.Create( nameof( CommandParameter ), typeof( object ), typeof( EventToCommandObserver ) );
    
        public static readonly BindableProperty EventArgsConverterProperty =
            BindableProperty.Create( nameof( EventArgsConverter ), typeof( IValueConverter ),
                typeof( EventToCommandObserver ) );
    
        public ICommand Command
        {
            get { return (ICommand)this.GetValue( CommandProperty ); }
            set { this.SetValue( CommandProperty, value ); }
        }
    
        public object CommandParameter
        {
            get { return this.GetValue( CommandParameterProperty ); }
            set { this.SetValue( CommandParameterProperty, value ); }
        }
    
        public IValueConverter EventArgsConverter
        {
            get { return (IValueConverter)this.GetValue( EventArgsConverterProperty ); }
            set { this.SetValue( EventArgsConverterProperty, value ); }
        }
    
        public static string GetEventName( BindableObject bindable )
        {
            return (string)bindable.GetValue( EventNameProperty );
        }
    
        public static void SetEventName( BindableObject bindable, string value )
        {
            bindable.SetValue( EventNameProperty, value );
        }
    
        private static void OnEventNameChanged( BindableObject bindable, object oldValue, object newValue )
        {
            DeregisterEvent( oldValue as string, bindable );
    
            RegisterEvent( newValue as string, bindable );
        }
    
        private static void RegisterEvent( string name, object associatedObject )
        {
            if ( string.IsNullOrWhiteSpace( name ) )
            {
                return;
            }
    
            EventInfo eventInfo = associatedObject.GetType().GetRuntimeEvent( name );
    
            if ( eventInfo == null )
            {
                throw new ArgumentException( $"EventToCommandBehavior: Can't register the '{name}' event." );
            }
    
            MethodInfo methodInfo = typeof( EventToCommandObserver ).GetTypeInfo().GetDeclaredMethod( "OnEvent" );
    
            Delegate eventHandler = methodInfo.CreateDelegate( eventInfo.EventHandlerType );
    
            eventInfo.AddEventHandler( associatedObject, eventHandler );
        }
    
        private static void DeregisterEvent( string name, object associatedObject )
        {
            if ( string.IsNullOrWhiteSpace( name ) )
            {
                return;
            }
    
            EventInfo eventInfo = associatedObject.GetType().GetRuntimeEvent( name );
    
            if ( eventInfo == null )
            {
                throw new ArgumentException( $"EventToCommandBehavior: Can't de-register the '{name}' event." );
            }
    
            MethodInfo methodInfo =
                typeof( EventToCommandObserver ).GetTypeInfo().GetDeclaredMethod( nameof( OnEvent ) );
    
            Delegate eventHandler = methodInfo.CreateDelegate( eventInfo.EventHandlerType );
    
            eventInfo.RemoveEventHandler( associatedObject, eventHandler );
        }
    
        private static void OnEvent( object sender, object eventArgs )
        {
            if ( ( (View)sender ).Parent is EventToCommandObserver commandView )
            {
                ICommand command = commandView.Command;
    
                if ( command == null )
                {
                    return;
                }
    
                object resolvedParameter;
    
                if ( commandView.CommandParameter != null )
                {
                    resolvedParameter = commandView.CommandParameter;
                }
                else if ( commandView.EventArgsConverter != null )
                {
                    resolvedParameter =
                        commandView.EventArgsConverter.Convert( eventArgs, typeof( object ), null, null );
                }
                else
                {
                    resolvedParameter = eventArgs;
                }
    
                if ( command.CanExecute( resolvedParameter ) )
                {
                    command.Execute( resolvedParameter );
                }
            }
        }
    }
    

    And this alternate xaml embedded inside a DataTemplate:

            <my:EventToCommandObserver Grid.Column="1"
                                       Command="{Binding BindingContext.TextChangedCommand, Mode=OneTime, Source={RelativeSource FindAncestor, AncestorType={x:Type ItemsView}}}"
                                       CommandParameter="{Binding Mode=OneTime}">
                <Entry Text="{Binding Value}"
                       HorizontalTextAlignment="Start"
                       HorizontalOptions="FillAndExpand"
                       VerticalOptions="Center"
                       VerticalTextAlignment="Center"
                       Keyboard='Text'
                       ClearButtonVisibility="WhileEditing"
                       my:EventToCommandObserver .EventName="TextChanged" />
            </my:EventToCommandObserver >