【问题标题】:How to call Command from DataTemplate Entry TextChanged Event in Xamarin Forms?如何从 Xamarin 表单中的 DataTemplate 条目 TextChanged 事件调用命令?
【发布时间】:2023-03-27 20:15:01
【问题描述】:

在 CollectionView 中使用 DataTemplate... 我可以通过这样的按钮调用 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>

那么,为什么我不能像这样从条目的 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

谢谢!

【问题讨论】:

  • 你能把你的事件添加到命令行为中
  • 如果我明白你在问什么,上面的代码不会调用TestCommand。还有什么要检查的吗? xaml 没有任何代码,因为它是一个 DataTemplate ......当我尝试添加一个带有 cs 支持者的 DataTemplate 时,我遇到了各种各样的错误......这似乎不可能。有什么建议吗?
  • @ScumSprocket 你有什么错误信息?你的意思是当你使用Entry时它可以正常工作,但是在DataTemplate中使用EventToCommandBehavior时会出现问题?
  • 没有错误,只是事件发生时不调用命令。在遵循 MS Docs 时,我没有任何用于 DataTemplate 的 cs 代码隐藏,所以我不确定如何测试它。我在项目的另一个不涉及 DataTemplate 的区域中使用完全相同的 EventToCommandBehavior 代码,并且它可以工作。

标签: xamarin.forms mvvm datatemplate relativesource eventtocommand


【解决方案1】:

我查看了您的示例代码,您似乎正在使用Xamarin Forms Example code 来实现您的EventToCommandBehavior。这也在Xamarin Community Toolkit 中以大致相同的方式实现。 注意这些实现继承自Xamarin.Forms.Behavior

我还尝试使用这些示例在分配给ItemsViewDataTemplate 中执行相对源绑定,但是当我运行示例时(与您上面的示例相同),我会在以下位置收到InvalidOperationException

Xamarin.Forms.Binding.ApplyRelativeSourceBinding (Xamarin.Forms.BindableObject 目标对象, Xamarin.Forms.BindableProperty targetProperty) [0x0006c] 在 C:\Advanced Dev\Xamarin.Forms\Xamarin.Forms.Core\Binding.cs:158

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

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

FindAncestor 表示绑定元素的可视树中的祖先。

Self 表示要设置绑定的元素,

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

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

下面的代码模拟了一个观察者的想法,它持有命令并观察它的孩子的事件。父/观察者使用我们在Xamarin Forms Example code 中实现的Command/CommandParameter/Converter 扩展了乏味的Xamarin.Forms.ContentView 元素,并将其与EventName 的Attached Property 实现结合起来。

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

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 >

【讨论】:

  • 似乎答案就在那里,某处......我迫不及待地想回到我最喜欢的问题!
  • @ScumSprocket 抱歉,如果它变成了 TL;DR。没想到2个答案是要走的路。顶部部分描述了为什么 RelativeSource 绑定对 Behavior 不起作用,并建议了个别实现,这些实现并不是那么美妙但有效。这就是我开始的地方。底部提出了一个替代的和更通用的可能解决方案,我最终使用了它。
【解决方案2】:

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

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

   
}

【讨论】:

  • Bu,DataTemplate 位于 DataTemplateSelector 中……很抱歉,我没有让这一点更明显。我在 GitHub 上附加了一个项目来提供帮助。
  • @ScumSprocket 如果您在 github 上有一个解决方案,请在此处分享您的链接,谢谢。
  • 这里是一个项目...模板文件是MyDataTemplate1.xaml,你也可以看到我试过的CutomEntry。在 DataTemplate 中时,CustomEntry 根本不会出现。我添加了一些黄色指示器来显示轻按手势的工作...我不明白为什么更改的输入文本不会触发。 github.com/BullCityCabinets/DataTemplateEventIssue
  • @ScumSprocket 我试试你的示例,你的意思是 customentry 文本更改时 customentry TextChangedCommand 不会触发?
  • 正确。我可以让其他事情正常工作,只是不能在条目上使用 TextChanged。我实际上是在 C# 中重建页面以查看是否可以创建事件(因为 DataTemplate 没有 cs 文件)。
猜你喜欢
  • 2019-11-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-05
  • 1970-01-01
  • 1970-01-01
  • 2012-06-01
相关资源
最近更新 更多