2
votes

I'm working with Xamarin.Forms in a PCL project.

I have a page/screen where there are an ListView control. I have created a custom DataTemplate for ViewCell.

This ViewCell has different controls: some Labels, one Button and also a Entry.

<ListView x:Name="lvProducts" >
  <ListView.ItemTemplate>
    <DataTemplate>
      <ViewCell>
        <StackLayout BackgroundColor="#FFFFFF" Orientation="Vertical">
          <Grid Padding="5">
            ...
            <Button Grid.Row="0" Grid.Column="1" Text="X" 
                    CommandParameter="{Binding MarkReference}"
                    Clicked="DeleteItemClicked" />
            ...
            <StackLayout Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal" >
              <Label Text="Ref.: " FontSize="24" FontAttributes="Bold" TextColor="#000000" />
              <Label Text="{Binding Reference}" FontSize="24" TextColor="#000000" />
            </StackLayout>
            ...
            <Entry Grid.Row="3" Grid.Column="1" Text="{Binding NumElements}"
                   Keyboard="Numeric" Placeholder="" FontSize="24"
                   HorizontalTextAlignment="Center"  Focused="OnItemFocus"    
                   Unfocused="OnItemUnfocus" />
         </Grid>
       </StackLayout>   
      </ViewCell>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

I want to achieve two things with this Entry control that I'm not able achieve:

First, when I add a new item, I would like that this new item has his Entry the focus, ready to start typing.

Second, when the user ends to write a value into the Entry, I would like to change the value of the behind. I would like know which Entry of ListView has modified. I tried to use the Unfocused event, but in the params of the method that launches only has a sender param that returns the Entry object, no reference about the model that has binded.

    public void OnItemUnfocus(object sender, EventArgs e)
    {
        Entry entry = (Entry)sender;
        //Here I would like to know the model object that's binded 
        //with this Entry / CellView item
    }

How I can achieve these two points?

2
Regarding the second point, if you need to access the data object, you can override OnBindingContextChanged in the cell, and then obtain the object with var object = (YourClass)BindingContext. This way you can access the object in the data source whenever you need it.papafe
Thank you @markusian, I got your idea, and I have used it. I extended the Entry control and I used into Unfocused event. Works fine.stivex

2 Answers

3
votes

I'd like to suggest you to use behaviors:

public class FocusBehavior : Behavior<Entry>
{
    private Entry _entry;

    public static readonly BindableProperty IsFocusedProperty =
        BindableProperty.Create("IsFocused",
                                typeof(bool),
                                typeof(FocusBehavior),
                                default(bool),
                                propertyChanged: OnIsFocusedChanged);

    public int IsFocused
    {
        get { return (int)GetValue(IsFocusedProperty); }
        set { SetValue(IsFocusedProperty, value); }
    }

    protected override void OnAttachedTo(Entry bindable)
    {
        base.OnAttachedTo(bindable);

        _entry = bindable;
    }

    private static void OnIsFocusedChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var behavior = bindable as FocusBehavior;
        var isFocused = (bool)newValue;

        if (isFocused)
        {
            behavior._entry.Focus();
        }
    }
}

<ListView x:Name="TasksListView"
          ItemsSource={Binding Tasks}
          RowHeight="200">
  <ListView.ItemTemplate>
    <DataTemplate>
      <ViewCell x:Name="ViewCell">
        <Grid x:Name="RootGrid"
              Padding="10,10,10,0"
              BindingContext="{Binding}">
          <Entry>
              <Entry.Behaviors>
                <helpers:FocusBehavior IsFocused="{Binding BindingContext.IsFocused, Source={x:Reference RootGrid}}"/>
              </Entry.Behaviors>
          </Entry>
        </Grid>
      </ViewCell>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>

And my model:

public class TaskModel : INotifyPropertyChanged
{
    private bool _isFocused;

    public bool IsFocused
    {
        get { return _isFocused; }
        set
        {
            _isFocused = value;
            RaisePropertyChanged();
        }
    }

And in ViewModel, after adding new item, set it's IsFocused property to true.

The same thing with behavior you could use for TextChanged for Entry.

-1
votes

For my first question, I have found a way to solve it. I don't know if it is the best solution.

I have extended ListView to CustomListView, and I have added and a dictionary of cells:

private Dictionary<string, Cell> dicCells;

Also, I have overridden SetupContent and UnhookContent methods.

SetupContent fires when new cell has been added and one of his params gives me the new cell that I save into the dictionary. (MarkReference is my key value)

    //When a new cell item has added, we save it into a dictionary
    protected override void SetupContent(Cell content, int index)
    {
        base.SetupContent(content, index);

        ViewCell vc = (ViewCell)content;            

        if (vc != null)
        {
            BasketProduct bp = (BasketProduct)vc.BindingContext;
            if (bp != null)
            {
                this.dicCells.Add(bp.MarkReference, content);
            }                
        }

    }

UnhookContent fires when a cell has been removed. I remove the item that exists into my dictionary.

    //When a new cell item has removed, we remove from the dictionary
    protected override void UnhookContent(Cell content)
    {
        base.UnhookContent(content);

        ViewCell vc = (ViewCell)content;

        if (vc != null)
        {
            BasketProduct bp = (BasketProduct)vc.BindingContext;
            if (bp != null)
            {
                this.dicCells.Remove(bp.MarkReference);
            }
        }

    }

Then, I have created a function that retrieves a Entry (CustomEntry in my case) that contains the object (BasketProduct in my case).

    //Retrieves a CustomEntry control that are into the collection and represents the BasketProduct that we have passed
    public CustomEntry GetEntry(BasketProduct bp)
    {
        CustomEntry ce = null;

        if (bp != null && this.dicCells.ContainsKey(bp.MarkReference))
        {
            ViewCell vc = (ViewCell)this.dicCells[bp.MarkReference];

            if (vc != null)
            {
                ce = (CustomEntry)((Grid)((StackLayout)vc.View).Children[0]).Children[4];
            }

        }

        return ce;
    }

When I want to give the focus on a certain Entry, I call this method:

    //Put the focus on the CustomEntry control that represents de BasketProduct that they have passed
    public void SetSelected(BasketProduct bp, bool withDelay)
    {
        CustomEntry entry = null;

        entry = GetEntry(bp);

        if (entry != null)
        {
            if (withDelay)
            {
                FocusDelay(entry);
            } else
            {
                entry.Focus();
            }                                
        }            
    }

If I call the SetSelected() method from ItemTapped method, works fine, but if I call the SetSelected() method after adding a item in then collection, the Entry doesn't get the focus. In this case, I have done a trick.

    private async void FocusDelay(CustomEntry entry)
    {            
        await Task.Delay(500);
        entry.Focus();
    }

About second question, as @markusian suggested, I have extended the Entry (CustomEntry) control and in the Unfocused event I have done this:

    private void CustomEntry_Unfocused(object sender, FocusEventArgs e)
    {
        try
        {
            //If the user leaves the field empty, we set the last value
            BasketProduct bp = (BasketProduct)BindingContext;
            if (this.Text.Trim().Equals(string.Empty))
            {
                this.Text = bp.NumElements.ToString();
            }
        }
        catch (FormatException ex) { }
    }