Monday, July 30, 2018

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

Leave a Comment

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.

4 Answers

Answers 1

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

Answers 2

Please note that the simplest way to implement this would be through MVVM (i.e. 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}" /> 

Answers 3

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

Answers 4

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> 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment