7

I have this XAML code:

<TableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">
   <TableSection>
      <TableSection.Title>
         Card Selection
      </TableSection.Title>
      <ViewCell Height="50">
         <Grid>
            <Grid x:Name="deselectGridLink" VerticalOptions="CenterAndExpand" Padding="20, 0">
               <Label TextColor="Blue" Style="{DynamicResource ListItemTextStyle}" x:Name="deselectLink" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All" />
            </Grid>
            <Grid x:Name="deselectGridLabel" VerticalOptions="CenterAndExpand" Padding="20, 0">
               <Label TextColor="Silver" Style="{DynamicResource ListItemTextStyle}" x:Name="deselectLabel" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All" />
            </Grid>
         </Grid>
       </ViewCell>
       <ViewCell Height="50">
          <Grid x:Name="selectGridLink" VerticalOptions="CenterAndExpand" Padding="20, 0">
             <Label TextColor="Blue" Style="{DynamicResource ListItemTextStyle}" x:Name="selectLink" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Select All" />
           </Grid>
        </ViewCell>
   </TableSection>
</TableView>

When other parts of my code call: SetPageDetails() then the label in the grid is changed to a link or the link is changed to a label. So for this when it is a label I would like to have no background flash event and no action called.

I attach a tap gesture recognizer like this. Note it's all on one line but covers two lines so it's more visible here in the SO question:

deselectGridLink.GestureRecognizers
      .Add(NewTapGestureForUpdateCategories(false));

    private TapGestureRecognizer NewTapGestureForUpdateCategories(bool val)
    {
        return new TapGestureRecognizer()
        {
            Command = new Command(() =>
            {
                App.DB.UpdateAllCategoryGroups(val);
                App.DB.UpdateAllCategories(val);
                GetPageData();
                RemoveTableViewClickSection();
                tableView.Root.Add(CreateTableSection());
            })
        };
    }

When the user clicks the row when deselectGridLink grid is visible then:

  • The deselectGridLink visibility is set to false
  • The deselectGridLabel visibility is set to true

    private void SetPageDetails()
    {
        Title = App.cardCountForSelectedCategories + (App.cardCountForSelectedCategories == 1 ? " Card Selected" : " Cards Selected");
        if (App.cardCountForSelectedCategories == 0)
        {
            deselectGridLink.IsVisible = false;
            deselectGridLabel.IsVisible = true;
        }
        else
        {
            deselectGridLink.IsVisible = true;
            deselectGridLabel.IsVisible = false;
        }
    }
    

The effect of this is that the grid link text will change to silver and the link becomes a label.

However even though it's a gray color label when the label is clicked there is still a brief background row color change from white to a dark color when the label is clicked. I assume it's just the way a view cell works.

Is there a way to suppress this from happening?

Alan2
  • 23,493
  • 79
  • 256
  • 450

2 Answers2

14

EDIT 1 - Updated answer as per updates to question. i.e. add support for switching between highlight enabled/disabled mode.

EDIT 2 - Restructure answer and add more details.

Option-1: Enable/disable view-cell through IsEnabled

The simplest option would be to use the IsEnabled property, which in turn enables/disables the background flash behavior. The only downside to this approach is that it will also disable the taps on child controls, i.e. tap events/gesture recognizer(s) will not be triggered if parent view-cell's IsEnabled is false.

For example:

XAML

<!-- Add name attribute to view-cell -->
<ViewCell x:Name="deselectCell" ..>
    <Grid>
        <Grid x:Name="deselectGridLink" ..
    ....
</ViewCell>

Code-behind

private void SetPageDetails()
{
    if (App.cardCountForSelectedCategories == 0)
    {
        deselectCell.IsEnabled = false; //disable background flash
        ...
    }
    else
    {
        deselectCell.IsEnabled = true;
        ...
    }
}

Recommendation 1 - Use data-binding and triggers

Instead of controlling visibility for each label in code-behind, you can use triggers and data-binding as follows (view-model will have a IsDeselectEnabled property):

<ViewCell IsEnabled="{Binding IsDeselectEnabled}" Height="50">
    <Label Margin="20,0,20,0" Style="{DynamicResource ListItemTextStyle}" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All">
        <Label.Triggers>
            <DataTrigger TargetType="Label" Binding="{Binding IsDeselectEnabled}" Value="true">
                <Setter Property="TextColor" Value="Blue" />
            </DataTrigger>
            <DataTrigger TargetType="Label" Binding="{Binding IsDeselectEnabled}" Value="false">
                <Setter Property="TextColor" Value="Silver" />
            </DataTrigger>
        </Label.Triggers>
    </Label>
</ViewCell>

Recommendation 2 - Use triggers with view as source

<ViewCell x:Name="deselectCell" Height="50">
    <Label Margin="20,0,20,0" Style="{DynamicResource ListItemTextStyle}" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All">
        <Label.Triggers>
            <DataTrigger TargetType="Label" Binding="{Binding IsEnabled, Source={x:Reference deselectCell}}" Value="true">
                <Setter Property="TextColor" Value="Blue" />
            </DataTrigger>
            <DataTrigger TargetType="Label" Binding="{Binding IsEnabled, Source={x:Reference deselectCell}}" Value="false">
                <Setter Property="TextColor" Value="Silver" />
            </DataTrigger>
        </Label.Triggers>
    </Label>
</ViewCell>

Option-2: Enable/disable highlight, but allow taps

To allow taps while toggling ViewCell's background-highlight behavior, we will need to implement platform-renderer(s).

In case of iOS, we can use SelectionStyle to toggle this behavior, while in case of android, we can use Clickable property.

Shared control:

public class CustomViewCell : ViewCell
{
    public static readonly BindableProperty AllowHighlightProperty =
        BindableProperty.Create(
            "AllowHighlight", typeof(bool), typeof(CustomViewCell),
            defaultValue: true);

    public bool AllowHighlight
    {
        get { return (bool)GetValue(AllowHighlightProperty); }
        set { SetValue(AllowHighlightProperty, value); }
    }
}

iOS renderer:

[assembly: ExportRenderer(typeof(CustomViewCell), typeof(CustomViewCellRenderer))]
namespace SampleApp.iOS
{
    public class CustomViewCellRenderer : ViewCellRenderer
    {
        UITableViewCell _nativeCell;

        //get access to the associated forms-element and subscribe to property-changed
        public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
        {
            _nativeCell = base.GetCell(item, reusableCell, tv);

            var formsCell = item as CustomViewCell;

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

            //and, update the style 
            SetStyle(formsCell);

            return _nativeCell;
        }

        void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var formsCell = sender as CustomViewCell;
            if (formsCell == null)
                return;
            //TODO: Trying to find a nicer and more robust way to dispose and unsubscribe :(
            if (_nativeCell == null)
                formsCell.PropertyChanged -= OnPropertyChanged;

            if (e.PropertyName == CustomViewCell.AllowHighlightProperty.PropertyName)
            {
                SetStyle(formsCell);
            }
        }

        private void SetStyle(CustomViewCell formsCell)
        {
            //added this code as sometimes on tap, the separator disappears, if style is updated before tap animation finishes 
            //https://stackoverflow.com/questions/25613117/how-do-you-prevent-uitableviewcellselectionstylenone-from-removing-cell-separato
            Device.StartTimer(TimeSpan.FromMilliseconds(50), () => {
                Device.BeginInvokeOnMainThread(() =>
                {
                    if (formsCell.AllowHighlight)
                        _nativeCell.SelectionStyle = UITableViewCellSelectionStyle.Default;
                    else
                        _nativeCell.SelectionStyle = UITableViewCellSelectionStyle.None;
                });
                return false;
            });
        }
    }
}

Android renderer:

[assembly: ExportRenderer(typeof(CustomViewCell), typeof(CustomViewCellRenderer))]
namespace SampleApp.Droid
{
    public class CustomViewCellRenderer : ViewCellRenderer
    {
        Android.Views.View _nativeCell;

        protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, Android.Views.ViewGroup parent, Android.Content.Context context)
        {
            _nativeCell = base.GetCellCore(item, convertView, parent, context);

            SetStyle();

            return _nativeCell;
        }

        // this one is simpler as the base class has a nice override-able method for our purpose - so we don't need to subscribe 
        protected override void OnCellPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnCellPropertyChanged(sender, e);

            if(e.PropertyName == CustomViewCell.AllowHighlightProperty.PropertyName)
            {
                SetStyle();
            }
        }

        private void SetStyle()
        {
            var formsCell = Cell as CustomViewCell;
            if (formsCell == null)
                return;

            _nativeCell.Clickable = !formsCell.AllowHighlight;
        }
    }
}

Sample usage 1 - Through data-binding

<local:CustomViewCell  AllowHighlight="{Binding IsHighlightEnabled}" ..>
    <Grid>
        <Grid x:Name="deselectGridLink" ..
    ...
</local:CustomViewCell>

Sample usage 2 - Through code-behind

XAML

<!-- Add name attribute to view-cell -->
<local:CustomViewCell x:Name="deselectCell" ..>
    <Grid>
        <Grid x:Name="deselectGridLink" ..
    ...
</local:CustomViewCell>

Code-behind

private void SetPageDetails()
{
    if (App.cardCountForSelectedCategories == 0)
    {
        deselectCell.AllowHighlight= false; //disable background flash
        ...
    }
    else
    {
        deselectCell.AllowHighlight= true;
        ...
    }
}

Option-3: Disable highlight, selection for all items

This particularly applies to ListView. The updated question now specifies that the cells are part of TableView, so this option is no longer valid in current question context.

You will need to implement platform renderers to disable highlight colors, and add ItemTapped handler to ListView to disable selection by setting SelectedItem as null always. References used:

  1. Disable highlight item
  2. Disable selection

Code

To get started, create a custom view-cell:

public class NoSelectViewCell : ViewCell { }

Implement iOS renderer as:

[assembly: ExportRenderer(typeof(NoSelectViewCell), typeof(NoSelectViewCellRenderer))]
namespace SampleApp.iOS
{
    public class NoSelectViewCellRenderer : ViewCellRenderer
    {
        public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
        {
            var nativeCell = base.GetCell(item, reusableCell, tv);
            nativeCell.SelectionStyle = UITableViewCellSelectionStyle.None;
            return nativeCell;
        }
    }
}

Implement android renderer as:

[assembly: ExportRenderer(typeof(NoSelectViewCell), typeof(NoSelectViewCellRenderer))]
namespace SampleApp.Droid
{
    public class NoSelectViewCellRenderer : ViewCellRenderer
    {
        protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, Android.Views.ViewGroup parent, Android.Content.Context context)
        {
            var cell = base.GetCellCore(item, convertView, parent, context);

            cell.Focusable = false;
            cell.FocusableInTouchMode = false;

            var listView = parent as Android.Widget.ListView;
            if (listView != null)
            {
                listView.SetSelector(Android.Resource.Color.Transparent);
                listView.CacheColorHint = Xamarin.Forms.Color.Transparent.ToAndroid();
            }
            return cell;
        }
    }
}

Sample Usage:

XAML

<ListView ItemTapped="Handle_ItemTapped">
    <ListView.ItemTemplate>
        <DataTemplate>
            <local:NoSelectViewCell Height="50">
               <Grid>
                  <Grid x:Name="deselectGridLink" VerticalOptions="CenterAndExpand" Padding="20, 0">
                     <Label TextColor="Blue" Style="{DynamicResource ListItemTextStyle}" x:Name="deselectLink" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All" />
                  </Grid>
                  <Grid x:Name="deselectGridLabel" VerticalOptions="CenterAndExpand" Padding="20, 0">
                     <Label TextColor="Silver" Style="{DynamicResource ListItemTextStyle}" x:Name="deselectLabel" HorizontalOptions="StartAndExpand" VerticalOptions="Center" Text="Deselect All" />
                  </Grid>
               </Grid>
            </local:NoSelectViewCell>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Code-behind

void Handle_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e)
{
    // don't do anything if we just de-selected the row
    if (e.Item == null) return;
    // do something with e.SelectedItem
    ((ListView)sender).SelectedItem = null; // de-select the row
}
Sharada Gururaj
  • 13,471
  • 1
  • 22
  • 50
  • Hi @G. Sharada - I am sorry I think my question was not clear enough. What I am looking for is just to be able to disable the background click effect if the view cell contents are for a label and not a link. With the code I switch it from being a label to a link dynamically so that once the user has clicked to remove all rows then the deselect then becomes a link. Can you have a look at the revised question and let me know if you think your solution could be adapted. In other words I think I need some way of specifying if that ViewCell has or does not have the background click. – Alan2 Sep 19 '17 at 11:50
  • 2
    @Alan2 : I think I understand it better now. I will the update the answer and let you know. – Sharada Gururaj Sep 19 '17 at 11:56
  • 1
    Hi @Alan2 : I have updated the answer as per my understanding. It has three options to choose from - the first one allows you to toggle highlight behavior from code-behind/view model while allowing taps, while the second option basically disables the row. Hope it helps! – Sharada Gururaj Sep 20 '17 at 04:24
  • Thanks I will check these out today and accept your answer or add something to the comments. – Alan2 Sep 20 '17 at 05:48
  • Hi @Alan2 - Just an FYI.. I have updated the answer in terms of structure and explanation. I am also planning to update iOS renderer code a little bit for more robust dispose logic. I will update you once I am able to do that. Thanks. – Sharada Gururaj Sep 21 '17 at 18:04
  • Thanks, I will wait a while then. Looking forward to seeing the changes. – Alan2 Sep 22 '17 at 09:23
  • Hi @Alan2 : from my analysis it looks like unless [`ViewTableCell`](https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Platform.iOS/Cells/ViewCellRenderer.cs#L48) is made public - we can't simplify the dispose/cleanup logic we have in the iOS renderer. If you are ok with using a `TextCell` instead of `ViewCell` - then I would recommend using property-change logic in this [answer](https://stackoverflow.com/a/45784618/7292772). – Sharada Gururaj Sep 24 '17 at 05:42
  • Hi, You mention it cannot be simplified. Do you think it's okay to leave it as you have coded then or do you think there will be a problem if I implement what you coded? I would really like to continue using ViewCells as we want to keep everything as much the same as possible. – Alan2 Sep 24 '17 at 06:02
  • Hi @Alan2 : Yes, we can use the code as it is - I was hoping to avoid subscribing to `PropertyChanged` event as `ViewTableCell` is already doing that. - but as the type is inaccessible - we can't reuse the logic in there – Sharada Gururaj Sep 24 '17 at 06:06
  • Thanks, I'll mark the answer as accepted. If I have more questions related to this I will create another question and reference this question of yours. – Alan2 Sep 24 '17 at 06:10
1

What G.Sharada proposes is very nicely working for iOS, but on Android I still had blinks on click. Adding this line to the styles file solved the problem.

<item name="android:colorActivatedHighlight">@android:color/transparent</item>
Eli
  • 414
  • 5
  • 10