如何从 Xamarin Forms 中的 DataTemplate 条目 TextChanged 事件调用命令?

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

在 CollectionView 中使用 DataTemplates... 我可以通过这样的按钮调用 ViewModel 的命令:

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

或者像这样的手势:

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

那么,为什么我不能像这样从 Entry 的 TextChanged 事件中调用该命令?

<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>  

EventToCommandBehavior 的代码在未在 DataTemplate

中使用时有效

这是一个说明问题的项目: https://github.com/BullCityCabinets/DataTemplateEventIssue

我从这些好人那里得到了按钮代码: https://www.syncfusion.com/kb/11029/how-to-bind-command-from-viewmodel-to-external-itemtemplate-of-xamarin-forms-listview

谢谢!

我测试了你的代码,如果我不使用相对源绑定,它可以正常工作。

<ContentPage
x:Class="demo3.simplecontrol2.Page2"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:customcontrol="clr-namespace:demo3.customcontrol"
xmlns:local="clr-namespace:demo3.simplecontrol2"
x:Name="root">
<ContentPage.Resources>
    <DataTemplate x:Key="datatemplate1">
        <StackLayout>
            <Entry Text="{Binding str, Mode=TwoWay}">
                <Entry.Behaviors>
                    <local:EventToCommandBehavior Command="{Binding Source={x:Reference root}, Path=BindingContext.command1}" EventName="TextChanged" />
                </Entry.Behaviors>
            </Entry>
        </StackLayout>

    </DataTemplate>
</ContentPage.Resources>
<ContentPage.Content>
    <StackLayout>
       

        <Entry Text="123">
            <Entry.Behaviors>
                <local:EventToCommandBehavior Command="{Binding command1}" EventName="TextChanged" />
            </Entry.Behaviors>
        </Entry>

        <CollectionView ItemTemplate="{StaticResource datatemplate1}" ItemsSource="{Binding entries}">
           
        </CollectionView>


      
    </StackLayout>
</ContentPage.Content>
 public partial class Page2 : ContentPage
{
    public ObservableCollection<testentry> entries { get; set; }

    
   public Command command1 { get; set; }

  
    public Page2()
    {
        InitializeComponent();

        entries = new ObservableCollection<testentry>()
        {
            new testentry(){str="test 1"},
            new testentry(){str="test 2"},
            new testentry(){str="test 3"},
            new testentry(){str="test 4"},
            new testentry(){str="test 5"},
            new testentry(){str="test 6"}

        };

        command1 = new Command(testcommand);
       
        

        this.BindingContext = this;

       
    }
   
    private void testcommand()
    {
       
        Console.WriteLine("this is test!");
    }

   
}

我查看了您的示例代码,您似乎以大致相同的方式使用 Xamarin Forms Example code to implement your EventToCommandBehavior. This is also implemented in the Xamarin Community Toolkit注意这些实现继承自Xamarin.Forms.Behavior

我还尝试了这些示例,以在分配给 ItemsViewDataTemplate 中执行相对源绑定,但是当我 运行 示例(与上面的示例相同)时,我会在:

收到 InvalidOperationException

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

转到 Xamarin source code,您可以看到抛出是绑定 targetObjectBinding.ApplyRelativeSourceBinding() 中应用绑定时未继承自 Xamarin.Forms.Element 的结果。由于 EventToCommandBehavior 继承自 Xamarin.Forms.Behavior 这就是结果。

Xamarin Relative Binding docs 没有特别提到 Binding Target 要求,他们显然关心绑定 Source .但他们确实提到这些绑定搜索视觉树或与元素相关:

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

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

由于 Behavior 不是 Element 并且不是视觉树的一部分(它存储在 VisualElement.Behaviors 属性 中),绑定不会在 运行 时间内无法直接访问任何一个来执行它的“搜索”,因此永远无法满足绑定。

我通过扩展 Entry 并在需要的地方添加命令解决了这个问题。这不是最可重用的解决方案,因为我必须在 Switch 等其他元素上执行此操作,但它有效。

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 );
    }
}

而xaml内嵌了一个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>

更新: 经过几天的实验,我发现了另一种可能的模式来以更通用的方式支持这一点。我会把它留给 reader 来决定它的优点。我目前倾向于认为这是一个合理的范例并且是通用的,因此不需要扩展一堆现有的视觉元素。

下面给出的代码模拟了持有命令并观察其子事件的观察者的想法。 parent/observer 使用我们在 EventName 的 Xamarin Forms Example code and combines that with an Attached Property 实现中看到的 Command/CommandParameter/Converter 扩展了乏味的 Xamarin.Forms.ContentView 元素。

ContentView.Content 属性 包含单个 Xamarin.Forms.View 对象,因此不会与附加的 属性 的目标混淆。事件处理程序都是静态的,因此不应该有任何泄漏问题。

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 );
            }
        }
    }
}

而这个替代 xaml 嵌入在 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 >