简体   繁体   中英

How can I pass a command to a template and have it execute in my back end code and pass the parameter?

I have this template:

<?xml version="1.0" encoding="utf-8"?>
<Grid Padding="20,0" xmlns="http://xamarin.com/schemas/2014/forms" 
      xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
      xmlns:local="clr-namespace:Japanese;assembly=Japanese" 
      x:Class="Japanese.Templates.DataGridTemplate" 
      x:Name="this" HeightRequest="49" Margin="0">
    <Grid.GestureRecognizers>
         <TapGestureRecognizer 
             Command="{Binding TapCommand, Source={x:Reference this}}"
             CommandParameter="1"
             NumberOfTapsRequired="1" />
    </Grid.GestureRecognizers>
    <Label Grid.Column="0" Text="{Binding Test" />
</Grid>

Behind this I have:

public partial class DataGridTemplate : Grid
{

    public DataGridTemplate()
    {
        InitializeComponent();
    }

    public static readonly BindableProperty TapCommandProperty =
       BindableProperty.Create(
           "Command",
           typeof(ICommand),
           typeof(DataGridTemplate),
           null);

    public ICommand TapCommand
    {
        get { return (ICommand)GetValue(TapCommandProperty); }
        set { SetValue(TapCommandProperty, value); }
    }

}

and I am trying to call the template like this in file: Settings.xaml.cs

<template:DataGridTemplate TapCommand="openCFSPage" />

hoping that it will call my method here in file: Settings.cs

void openCFSPage(object sender, EventArgs e)
{
    Navigation.PushAsync(new CFSPage());
}

The code compiles but when I click on the grid it doesn't call the openCFSPage method.

1) Does anyone have an idea what might be wrong?

2) Also is there a way that I can add a parameter to the template and then have that parameter passed to my method in the CS back end code?

Note that I would like to avoid adding a view model if possible. The application is small and I'd like to just have the code I need in the CS code of the page that calls the template.

You have 2 options depending on the the use case :

FYI, there's no way to call another method directly from the view (its a bad design pattern to do so)

  1. Using Event Aggregator :

Create interface

public interface IEventAggregator
{
    TEventType GetEvent<TEventType>() where TEventType : EventBase, new();
}

All you have to do is call it from you TapCommand

_eventAggregator.GetEvent<ItemSelectedEvent>().Publish(_selectedItem);

Then in your Settings.cs you can Create a method that can receive the data

 this.DataContext = new ListViewModel(ApplicationService.Instance.EventAggregator);
  1. Inheritance and Polymorphism / Making openCFSPage a service :

Creating a interface / service that links both models

public interface IOpenCFSPage
{
     Task OpenPage();
}

and a method :

public class OpenCFSPage : IOpenCFSPage
{
private INavigationService _navigationService;
public OpenCFSPage(INavigationService navigationService){
 _navigationService = navigationService;
}
        public async Task OpenPage()
        {
             await _navigationService.NavigateAsync(new CFSPage());
        }
}

Please note that the simplest way to implement this would be through MVVM (ie a view-model), but if you want to side-step this option (as you mentioned in the question) then you can use one of the following options

Option1 : Wrap delegate into command object

If you look at it from the perspective of a XAML parser, you are technically trying to assign a delegate to a property of type ICommand . One way to avoid the type mismatch would be to wrap the delegate inside a command-property in the page's code-behind.

Code-behind [Settings.xaml.cs]

ICommand _openCFSPageCmd;
public ICommand OpenCFSPageCommand {
    get {
        return _openCFSPageCmd ?? (_openCFSPageCmd = new Command(OpenCFSPage));
    }
}

void OpenCFSPage(object param)
{
    Console.WriteLine($"Control was tapped with parameter: {param}");
}

XAML [Settings.xaml]

<!-- assuming that you have added x:Name="_parent" in root tag -->
<local:DataGridView TapCommand="{Binding OpenCFSPageCommand, Source={x:Reference _parent}}" />

Option2 : Custom markup-extension

Another option (a bit less mainstream) is to create a markup-extension that wraps the delegate into a command object.

[ContentProperty("Handler")]
public class ToCommandExtension : IMarkupExtension
{
    public string Handler { get; set; }
    public object Source { get; set; }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        if (serviceProvider == null)
            throw new ArgumentNullException(nameof(serviceProvider));
        var lineInfo = (serviceProvider?.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo ?? new XmlLineInfo();


        object rootObj = Source;
        if (rootObj == null)
        {
            var rootProvider = serviceProvider.GetService<IRootObjectProvider>();
            if (rootProvider != null)
                rootObj = rootProvider.RootObject;
        }

        if(rootObj == null)
        {
            var valueProvider = serviceProvider.GetService<IProvideValueTarget>();
            if (valueProvider == null)
                throw new ArgumentException("serviceProvider does not provide an IProvideValueTarget");

            //we assume valueProvider also implements IProvideParentValues
            var propInfo = valueProvider.GetType()
                                        .GetProperty("Xamarin.Forms.Xaml.IProvideParentValues.ParentObjects", 
                                                     BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            if(propInfo == null)
                throw new ArgumentException("valueProvider does not provide an ParentObjects");

            var parentObjects = propInfo.GetValue(valueProvider) as IEnumerable<object>;
            rootObj = parentObjects?.LastOrDefault();
        }

        if(rootObj != null)
        {
            var delegateInfo = rootObj.GetType().GetMethod(Handler,
                                                           BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            if(delegateInfo != null)
            {
                var handler = Delegate.CreateDelegate(typeof(Action<object>), rootObj, delegateInfo) as Action<object>;
                return new Command((param) => handler(param));
            }
        }

        throw new XamlParseException($"Can not find the delegate referenced by `{Handler}` on `{Source?.GetType()}`", lineInfo);        
    }
}

Sample usage

<local:DataGridView TapCommand="{local:ToCommand OpenCFSPage}" />

Settings.xaml:

<template:DataGridTemplate TapCommand="{Binding OpenCFSPage}" />

<!-- Uncomment below and corresponding parameter property code in DataGridTemplate.xaml.cs to pass parameter from Settings.xaml -->
<!--<template:DataGridTemplate TapCommand="{Binding OpenCFSPage}" CommandParameter="A" />-->

Settings.xaml.cs:

public Settings()
{
    InitializeComponent();

    OpenCFSPage = new Command(p => OpenCFSPageExecute(p));

    BindingContext = this;
}

public ICommand OpenCFSPage { get; private set; }

void OpenCFSPageExecute(object p)
{
    var s = p as string;
    Debug.WriteLine($"OpenCFSPage:{s}:");
}

DataGridTemplate.xaml:

<?xml version="1.0" encoding="UTF-8"?>
    <Grid xmlns="http://xamarin.com/schemas/2014/forms" 
         xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
         xmlns:local="clr-namespace:Japanese;assembly=Japanese" 
         Padding="0,20" 
         HeightRequest="49" Margin="0"
         x:Class="Japanese.DataGridTemplate">
    <Grid.GestureRecognizers>
        <TapGestureRecognizer 
             Command="{Binding TapCommand}"
             CommandParameter="1"
             NumberOfTapsRequired="1" />
    </Grid.GestureRecognizers>
    <Label Grid.Column="0" Text="Test" />
</Grid>

DataGridTemplate.xaml.cs:

public partial class DataGridTemplate : Grid
{
    public DataGridTemplate()
    {
        InitializeComponent();
    }

    public static readonly BindableProperty TapCommandProperty = 
        BindableProperty.Create(
            nameof(TapCommand), typeof(ICommand), typeof(DataGridTemplate), null,
            propertyChanged: OnCommandPropertyChanged);

    public ICommand TapCommand
    {
        get { return (ICommand)GetValue(TapCommandProperty); }
        set { SetValue(TapCommandProperty, value); }
    }

    //public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create(
    //    nameof(CommandParameter), typeof(string), typeof(DataGridTemplate), null);

    //public string CommandParameter
    //{
    //    get { return (string)GetValue(CommandParameterProperty); }
    //    set { SetValue(CommandParameterProperty, value); }
    //}

    static TapGestureRecognizer GetTapGestureRecognizer(DataGridTemplate view)
    {
        var enumerator = view.GestureRecognizers.GetEnumerator();
        while (enumerator.MoveNext())
        {
            var item = enumerator.Current;
            if (item is TapGestureRecognizer) return item as TapGestureRecognizer;
        }
        return null;
    }

    static void OnCommandPropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is DataGridTemplate view)
        {
            var tapGestureRecognizer = GetTapGestureRecognizer(view);
            if (tapGestureRecognizer != null)
            {
                tapGestureRecognizer.Command = (ICommand)view.GetValue(TapCommandProperty);
                //tapGestureRecognizer.CommandParameter = (string)view.GetValue(CommandParameterProperty);
            }
        }
    }
}

Check this code you help you. Here you have to pass a reference of list view and also you need to bind a command with BindingContext.

 <ListView ItemsSource="{Binding Sites}" x:Name="lstSale">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout Orientation="Vertical">
                            <Label Text="{Binding FriendlyName}" />
                            <Button Text="{Binding Name}"
                                    HorizontalOptions="Center"
                                    VerticalOptions="Center"
                                    Command="{Binding 
                                   Path=BindingContext.RoomClickCommand,
                                    Source={x:Reference lstSale}}" 
                                   CommandParameter="{Binding .}" />
                       </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM