Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

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.

like image 584
Samantha J T Star Avatar asked Jul 28 '17 11:07

Samantha J T Star


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

like image 194
Juan Roman Escamilla Avatar answered Oct 22 '22 10:10

Juan Roman Escamilla


(Sorry for the poor english)

Despite not being best practice, I guess you can do something like this, dismissing the viewmodel:

XAML:

<ViewCell x:Name="ati" Tapped="OpenPickerCommand">
   <Grid VerticalOptions="CenterAndExpand" Padding="20, 0">
      <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">
      <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#:

private void OpenPickerCommand(object sender, System.EventArgs e)
{
    if (sender != null)
    {
        Picker pkr = sender == ati ? atiPicker : ptiPicker;
        pkr.Focus();
    }
}

Answering your question "Can the view model have one method that takes an argument?", it is exactly what you're already doing using the 'OpenPickerCommand' method. The problem is that using the ViewCell's public event 'Tapped', you can't set parameters to the delegate handler.

Let me know if it works for you or if you do need some more information.

I hope it helps.

like image 2
Diego Rafael Souza Avatar answered Oct 22 '22 10:10

Diego Rafael Souza