How can I make the Tapped event of a ViewCell send a param to a generic function and then open up a picker for that ViewCell element?

Viewed 6203

Update: Just a reminder, there's a 500 point bonus on this if someone can just show me how to implement this functionality without using Gestures>

I am using a ViewCell and a gesture recognizer to open up a picker with the following code. The ViewCell has a label on the left and a label area on the right that is populated initially when the app starts and later with the picker when the ViewCell is clicked.

XAML

<ViewCell x:Name="ati" Tapped="OpenPickerCommand">
   <Grid VerticalOptions="CenterAndExpand" Padding="20, 0">
      <Grid.GestureRecognizers>
         <TapGestureRecognizer 
             Command="{Binding OpenPickerCommand}" 
             CommandParameter="{x:Reference atiPicker}" NumberOfTapsRequired="1" />
      </Grid.GestureRecognizers>
      <local:LabelBodyRendererClass Text="Answer Time Interval" HorizontalOptions="StartAndExpand" />
      <Picker x:Name="atiPicker" IsVisible="false" HorizontalOptions="End" SelectedIndexChanged="atiPickerSelectedIndexChanged" ItemsSource="{Binding Times}"></Picker>
      <local:LabelBodyRendererClass x:Name="atiLabel" HorizontalOptions="End"/>
   </Grid>
</ViewCell>

<ViewCell x:Name="pti" Tapped="OpenPickerCommand">
   <Grid VerticalOptions="CenterAndExpand" Padding="20, 0">
      <Grid.GestureRecognizers>
         <TapGestureRecognizer 
             Command="{Binding OpenPickerCommand}" 
             CommandParameter="{x:Reference ptiPicker}" NumberOfTapsRequired="1" />
      </Grid.GestureRecognizers>
      <local:LabelBodyRendererClass Text="Phrase Time Interval" HorizontalOptions="StartAndExpand" />
      <Picker x:Name="ptiPicker" IsVisible="false" HorizontalOptions="End" SelectedIndexChanged="ptiPickerSelectedIndexChanged" ItemsSource="{Binding Times}"></Picker>
      <local:LabelBodyRendererClass x:Name="ptiLabel" HorizontalOptions="End"/>
   </Grid>
</ViewCell>

C# This works for different pickers (ati, bti, pti etc) with CommandParameter

public SettingsPage()
{
   InitializeComponent();
   BindingContext = new CommandViewModel();
}

void atiPickerSelectedIndexChanged(object sender, EventArgs e)
    {
        var picker = (Picker)sender;
        int selectedIndex = picker.SelectedIndex;
        if (selectedIndex != -1)
        {
            App.DB.UpdateIntSetting(Settings.Ati, selectedIndex);
            atiLabel.Text = AS.ati.Text();
        }
    }

void ptiPickerSelectedIndexChanged(object sender, EventArgs e)
    {
        var picker = (Picker)sender;
        int selectedIndex = picker.SelectedIndex;
        if (selectedIndex != -1)
        {
            App.DB.UpdateIntSetting(Settings.Pti, selectedIndex);
            ptiLabel.Text = AS.pti.Text();
        }
    }


public class CommandViewModel: ObservableProperty
{
    public ICommand openPickerCommand;

    public CommandViewModel()
    {
        openPickerCommand = new Command<Picker>(PickerFocus);
        //openPickerCommand = new Command(tapped);
    }

    public ICommand OpenPickerCommand
    {
        get { return openPickerCommand; }
    }

    void PickerFocus(Picker param)
    {
        param.Focus();
    }
}

I would like to remove the use of TapGestureRecognizers but I still want to retain the functionality and layout.

It's been suggested to me that it would be better if I used the Tapped event of the ViewCell like this:

 Tapped="OnTapped"

Can someone explain in some detail how I could wire this up in C#. Would I be best to code something into the CommandViewModel as well as in the C# backing code. Also can the view model have one method that takes an argument so it could be used to open up different pickers?

An example of how I could do this would be very much appreciated. Note that I don't particularly need to use the CommandViewModel if there is a way that I could do this by coding just in the .cs backing code.

3 Answers

You can solve this with attached properties. Simply define a "behavior" class for ViewCell that adds the Command/Parameter properties.

public static class TappedCommandViewCell
{
    private const string TappedCommand = "TappedCommand";
    private const string TappedCommandParameter = "TappedCommandParameter";

    public static readonly BindableProperty TappedCommandProperty =
        BindableProperty.CreateAttached(
            TappedCommand,
            typeof(ICommand),
            typeof(TappedCommandViewCell),
            default(ICommand),
            BindingMode.OneWay,
            null,
            PropertyChanged);

    public static readonly BindableProperty TappedCommandParameterProperty =
        BindableProperty.CreateAttached(
            TappedCommandParameter,
            typeof(object),
            typeof(TappedCommandViewCell),
            default(object),
            BindingMode.OneWay,
            null);

    private static void PropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is ViewCell cell)
        {
            cell.Tapped -= ViewCellOnTapped;
            cell.Tapped += ViewCellOnTapped;
        }
    }

    private static void ViewCellOnTapped(object sender, EventArgs e)
    {
        if (sender is ViewCell cell && cell.IsEnabled)
        {
            var command = GetTappedCommand(cell);
            var parameter = GetTappedCommandParameter(cell);

            if (command != null && command.CanExecute(parameter))
            {
                command.Execute(parameter);
            }
        }
    }

    public static ICommand GetTappedCommand(BindableObject bindableObject) =>
        (ICommand)bindableObject.GetValue(TappedCommandProperty);

    public static void SetTappedCommand(BindableObject bindableObject, object value) =>
        bindableObject.SetValue(TappedCommandProperty, value);

    public static object GetTappedCommandParameter(BindableObject bindableObject) =>
        bindableObject.GetValue(TappedCommandParameterProperty);

    public static void SetTappedCommandParameter(BindableObject bindableObject, object value) =>
        bindableObject.SetValue(TappedCommandParameterProperty, value);
}

After that reference your behavior namespace in XAML and specify the property values using fully qualified names:

<ViewCell StyleId="disclosure-indicator"
          behaviors:TappedCommandViewCell.TappedCommand="{Binding BrowseCommand}"
          behaviors:TappedCommandViewCell.TappedCommandParameter="https://www.google.com">
    <StackLayout Orientation="Horizontal">
        <Label Text="Recipient"
               VerticalOptions="Center"
               Margin="20,0"/>
        <Label Text="{Binding LedgerRecord.Recipient}"
               HorizontalOptions="EndAndExpand"
               VerticalOptions="Center"
               Margin="0,0,20,0"/>
        </Label>
    </StackLayout>
</ViewCell>

The above will allow you to use MVVM and no Tap Gesture Recognizers.

Related