Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I implement a click to select using a ViewModel

I have this XAML that displays 6 TextCells which can show a check mark or not. They also so as enabled or not enabled:

<TableSection Title="Front Side" x:Name="cfsSection">
   <local:CustomTextCell Text="{Binding [0].Name}" IsChecked="{Binding [0].IsSelected}" IsEnabled="{Binding [0].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
   <local:CustomTextCell Text="{Binding [1].Name}" IsChecked="{Binding [1].IsSelected}" IsEnabled="{Binding [1].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
   <local:CustomTextCell Text="{Binding [2].Name}" IsChecked="{Binding [2].IsSelected}" IsEnabled="{Binding [2].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
   <local:CustomTextCell Text="{Binding [3].Name}" IsChecked="{Binding [3].IsSelected}" IsEnabled="{Binding [3].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
   <local:CustomTextCell Text="{Binding [4].Name}" IsChecked="{Binding [4].IsSelected}" IsEnabled="{Binding [4].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
   <local:CustomTextCell Text="{Binding [5].Name}" IsChecked="{Binding [5].IsSelected}" IsEnabled="{Binding [5].IsSelected, Converter={StaticResource InverseBoolConverter} } "Tapped="cfsSelectValue"/>
</TableSection>

The code behind is I think fairly simple. It declares an array of SSVViewModel and the binding causes the text to display:

    SSVViewModel[] CFS = new[] {
        new SSVViewModel {Id = 0, Name=LANG.English.Text(), IsSelected = false},
        new SSVViewModel {Id = 1, Name=LANG.Romaji.Text(), IsSelected = false},
        new SSVViewModel {Id = 2, Name=LANG.Kana.Text(), IsSelected = false},
        new SSVViewModel {Id = 3, Name=LANG.Kanji.Text(), IsSelected = false},
        new SSVViewModel {Id = 4, Name=LANG.KanjiKana.Text(), IsSelected = false},
        new SSVViewModel {Id = 5, Name=LANG.KanaKanji.Text(), IsSelected = false},
    };

When a cell is clicked this function is called:

void cfsSelectValue(object sender, EventArgs e) 
{
        var cell = sender as TextCell;
        if (cell == null)
            return;

        var selected = cell.Text;

        foreach (var setting in CFS)
            setting.IsSelected = false;

        foreach (var setting in CFS)
            if (setting.Name == selected)
                setting.IsSelected = true;

 }

However when clicking on either of the first two cells both show as checked. All other cell clicks work fine. In another part of my code I use a similar construct and it's the last two cells that do not work.

Note that the IsEnabled works but not the IsChecked

Can anyone see why a click on the first two cells could possibly give any problem. I have been through this with the debugger many time but I still cannot see what might be wrong. Surely the code that sets the IsSelected to false should cause all except the one cell to show as checked.

Note that when debugging this line: setting.IsSelected = false; and this line: setting.IsSelected = true; then everything appears as it should as the correct cell has it's IsSelected set to true and the others to false. It's just when I look at the display it seems like the binding didn't work for those first two cells.

Here's the viewModel code I am using:

public class SSVViewModel: ObservableProperty
{
    private int id;
    private string name;
    private bool isSelected;

    public int Id
    {
        get
        {
            return id;
        }
        set
        {
            if (value != id)
            {
                id = value;
                NotifyPropertyChanged("Id");
            }
        }
    }

    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            if (value != name)
            {
                name = value;
                NotifyPropertyChanged("Name");
            }
        }
    }

    public bool IsSelected
    {
        get
        {
            return isSelected;
        }
        set
        {
            if (value != isSelected)
            {
                isSelected = value;
                NotifyPropertyChanged("IsSelected");
            }
        }
    }
}

Here is the code for the CustomTextCellRenderer

public class CustomTextCellRenderer : TextCellRenderer
{
    UITableViewCell _nativeCell;

    public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
    {
        _nativeCell = base.GetCell(item, reusableCell, tv);
        var formsCell = item as CustomTextCell;

        if (formsCell != null)
        {
            formsCell.PropertyChanged -= OnPropertyChanged;
            formsCell.PropertyChanged += OnPropertyChanged;
        }

        SetCheckmark(formsCell);
        SetTap(formsCell);

        return _nativeCell;
    }

    void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        var formsCell = sender as CustomTextCell;
        if (formsCell == null)
            return;

        if (e.PropertyName == CustomTextCell.IsCheckedProperty.PropertyName)
        {
            SetCheckmark(formsCell);
        }

        if (e.PropertyName == CustomTextCell.NoTapProperty.PropertyName)
        {
            SetTap(formsCell);
        }
    }

    private void SetCheckmark(CustomTextCell formsCell)
    {
        if (formsCell.IsChecked)
            _nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
        else
            _nativeCell.Accessory = UITableViewCellAccessory.None;
    }

    private void SetTap(CustomTextCell formsCell)
    {
        if (formsCell.NoTap)
            _nativeCell.SelectionStyle = UITableViewCellSelectionStyle.None;
        else
            _nativeCell.SelectionStyle = UITableViewCellSelectionStyle.Default;
    }

}

Update 1

<local:CustomTextCell           Text="{Binding [0].Name}" IsChecked="{Binding [0].IsSelected}" Tapped="cfsSelectValue" CommandParameter="0" />
<local:CustomTextCell           Text="{Binding [1].Name}" IsChecked="{Binding [1].IsSelected}" Tapped="cfsSelectValue" CommandParameter="1" />
<local:CustomTextCell           Text="{Binding [2].Name}" IsChecked="{Binding [2].IsSelected}" Tapped="cfsSelectValue" CommandParameter="2" />
<local:CustomTextCell           Text="{Binding [3].Name}" IsChecked="{Binding [3].IsSelected}" Tapped="cfsSelectValue" CommandParameter="3" />
<local:CustomTextCell           Text="{Binding [4].Name}" IsChecked="{Binding [4].IsSelected}" Tapped="cfsSelectValue" CommandParameter="4" />

Since the question was written I have stopped using this for the Lang selection however it's still used in another part of the code and I tried to put in some debug points. Here's what I did:

I added this to the iOS custom renderer:

    private void SetCheckmark(CustomTextCell formsCell)
    {
        if (formsCell.IsChecked)
        {
            _nativeCell.Accessory = UITableViewCellAccessory.Checkmark;
            Debug.WriteLine(_nativeCell.TextLabel.Text + " checked");
        }
        else
        {
            _nativeCell.Accessory = UITableViewCellAccessory.None;
            Debug.WriteLine(_nativeCell.TextLabel.Text + " unchecked");
        }
    }

Here's the result when I clicked JLPT N2:

Category Group unchecked
Category unchecked
All Available Words unchecked
Japanese for Busy People 1 unchecked
Japanese for Busy People 2 unchecked
Japanese for Busy People 3 unchecked
JLPT Level N5 unchecked
JLPT Level N4 unchecked
JLPT Level N3 unchecked
JLPT Level N2 unchecked
JLPT Level N1 checked
JLPT Level N2 checked
JLPT Level N3 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N2 checked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked
JLPT Level N1 unchecked

This was not at all what I expected.

On the screen I see that as expected N2 is disabled but there is a check mark next to N3 and N2.

Not sure if this helps but I notice the iOS renderer code used is different from similar code I have used in other places. For example here's a different iOS renderer. Code looks very different. I realize function is different but this one has things like cell = tv.DequeueReusableCell(fullName) as CellTableViewCell;

public class TextCellCustomRenderer : TextCellRenderer
{
    CellTableViewCell cell;
    public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
    {
        var textCell = (TextCell)item;
        var fullName = item.GetType().FullName;
        cell = tv.DequeueReusableCell(fullName) as CellTableViewCell;

        if (cell == null)
        {
            cell = new CellTableViewCell(UITableViewCellStyle.Value1, fullName);
        }
        else
        {
            cell.Cell.PropertyChanged -= cell.HandlePropertyChanged;
            //cell.Cell.PropertyChanged -= Current_PropertyChanged;
        }

        cell.Cell = textCell;
        textCell.PropertyChanged += cell.HandlePropertyChanged;
        cell.PropertyChanged = this.HandlePropertyChanged;
        cell.SelectionStyle = UITableViewCellSelectionStyle.None;
        cell.TextLabel.Text = textCell.Text;
        cell.DetailTextLabel.Text = textCell.Detail;
        cell.ContentView.BackgroundColor = UIColor.White;


        switch (item.StyleId)
        {
            case "checkmark":
                cell.Accessory = UIKit.UITableViewCellAccessory.Checkmark;
                break;
            case "detail-button":
                cell.Accessory = UIKit.UITableViewCellAccessory.DetailButton;
                break;
            case "detail-disclosure-button":
                cell.Accessory = UIKit.UITableViewCellAccessory.DetailDisclosureButton;
                break;
            case "disclosure":
                cell.Accessory = UIKit.UITableViewCellAccessory.DisclosureIndicator;
                break;
            case "none":
            default:
                cell.Accessory = UIKit.UITableViewCellAccessory.None;
                break;
        }

        //UpdateBackground(cell, item);

        return cell;
    }

    void checkAccessoryVisibility() {

    }

}
like image 201
Alan2 Avatar asked Nov 03 '17 09:11

Alan2


2 Answers

The code looks right to me - the only reason I would imagine this happening is in case the string comparison logic is not working as expected.

Maybe a reference based comparison might solve the issue. i.e Change your XAML to:

<TableSection Title="Front Side" x:Name="cfsSection">
   <local:CustomTextCell BindingContext="{Binding [0]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
   <local:CustomTextCell BindingContext="{Binding [1]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
   <local:CustomTextCell BindingContext="{Binding [2]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
   <local:CustomTextCell BindingContext="{Binding [3]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
   <local:CustomTextCell BindingContext="{Binding [4]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
   <local:CustomTextCell BindingContext="{Binding [5]}" Text="{Binding Name}" IsChecked="{Binding IsSelected}" Tapped="cfsSelectValue"/>
</TableSection>

and tapped handler to:

void cfsSelectValue(object sender, EventArgs e)
{
    var cell = sender as TextCell;
    if (cell == null)
        return;

    var selected = cell.BindingContext;

    foreach (var setting in CFS)
        setting.IsSelected = (setting == selected);

}
like image 54
Sharada Gururaj Avatar answered Nov 07 '22 17:11

Sharada Gururaj


I could not find the error causing that behaviour so here are my thoughts on this:

The behaviour you are describing sounds more like a RadioButton. Since there are none in Xamarin.Forms you could create your own, use a package or get a workaround.

Most easy workaround would be in your cfsSelectValue.

You could search the visual tree for all elements of local:CustomTextCells and set IsSelected to false for each TextCell that is not the one passed as the sender.


How I would do it:

Things to try out:

For your ViewModels use an ObservableCollection rather than an Array

private ObservableCollection<SSVViewModel> viewModels = new ObservableCollection<SSVViewModel>()
{
    new SSVViewModel {Id = 0, Name=LANG.English.Text()},
    new SSVViewModel {Id = 1, Name=LANG.Romaji.Text()},
    new SSVViewModel {Id = 2, Name=LANG.Kana.Text()},
    new SSVViewModel {Id = 3, Name=LANG.Kanji.Text()},
    new SSVViewModel {Id = 4, Name=LANG.KanjiKana.Text()},
    new SSVViewModel {Id = 5, Name=LANG.KanaKanji.Text()},
};

Derive your ViewModel from INotifyPropertyChanged rather than ObservableProperty

public class SSVViewModel : INotifyPropertyChanged
{
    private int id;
    private string name;
    private bool isSelected = false; //Set the default here

    public int Id
    {
        get
        {
            return id;
        }
        set
        {
            if (value != id)
            {
                id = value;
                OnPropertyChanged();
            }
        }
    }

    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            if (value != name)
            {
                name = value;
                OnPropertyChanged();
            }
        }
    }

    public bool IsSelected
    {
        get
        {
            return isSelected;
        }
        set
        {
            if (value != isSelected)
            {
                isSelected = value;
                OnPropertyChanged();
            }
        }
    }


    #region Implementation of INotifyPropertyChanged

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged([CallerMemberName] string propertyname = null)
    {
        if(PropertyChanged != null)
        {
            PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyname));
        }
    }

    #endregion
} 

Change your cfsSelectValue to this:

public void cfsSelectValue(object sender, EventArgs e)
{
    //GetCurrentCell
    CustomTextCell cell = sender as CustomTextCell;
    if (cell == null)
    {
        return;
    }            

    foreach (SSVViewModel viewModel in CFS)
    {
        /*
        Since there is no Tag Property we gotta use something different
        you could use `CommandParameter` since it is of type object

        */
        if (viewModel.Name == cell.Text && viewModel.Id == int.Parse(cell.CommandParameter.ToString()))
        {
            viewModel.IsSelected = true;
        }
        else
        {
            viewModel.IsSelected = false;
        }                    
    }
}
like image 44
Felix D. Avatar answered Nov 07 '22 16:11

Felix D.