0

I have what seems like a relatively simple problem, but I can't figure it out.

I have a Xamarin ListView, which is using an item template for the ViewCells as follows:

<ListView  HasUnevenRows="True"
           ItemsSource="{Binding Devices}" 
           ItemTemplate="{StaticResource DeviceDataTemplateSelector}"
           SelectionMode="None"
           ItemTapped="NewItemTapped"
           />

For now, 'DeviceDataTemplateSelector' always returns the following ViewCell (more ViewCells will be added later).

The ViewCell uses the Expander control from the Xamarin Community Toolkit

<ViewCell xmlns="http://xamarin.com/schemas/2014/forms" 
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
        xmlns:ViewModel="clr-namespace:App.ViewModels"
        x:Class="App.UI.ViewCellStandardTemplate">

    <ViewCell.View>
        
        <!-- Calls an expander for each ViewCell. -->
        <xct:Expander HorizontalOptions="FillAndExpand"
                        VerticalOptions="FillAndExpand"
                        Margin="{StaticResource UniversalMargin}" 
                        >

            <xct:Expander.Header>
                <Grid HorizontalOptions="FillAndExpand" 
                        VerticalOptions="FillAndExpand"
                        Margin="{StaticResource UniversalMargin}">

                    <Grid.RowDefinitions>
                        <RowDefinition Height = "*" />
                    </Grid.RowDefinitions>

                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width = "*" />
                    </Grid.ColumnDefinitions>

                    <Label Grid.Row="0" Grid.Column="0" Text="Header Text" />

                </Grid>
            </xct:Expander.Header>

            <xct:Expander.Content>
                <Grid HorizontalOptions="FillAndExpand" 
                        VerticalOptions="FillAndExpand"
                        Margin="{StaticResource UniversalMargin}">

                    <Grid.RowDefinitions>
                        <RowDefinition Height = "*" />
                    </Grid.RowDefinitions>
                    
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width = "*" />
                    </Grid.ColumnDefinitions>

                    <Button Grid.Row="0"
                            Grid.Column="0" 
                            Text="Click Me" 
                            Command="{Binding Source={RelativeSource AncestorType={x:Type ViewModel:PageViewModel}, Mode=FindAncestorBindingContext}, Path=ClickCommand}"
                            CommandParameter="{Binding .}"
                            />

                </Grid>
            </xct:Expander.Content>

        </xct:Expander>

    </ViewCell.View>

</ViewCell>

So far all of this works correctly - the issue is that when I tap an item in the ListView, I want any other expanded items to collapse.

In other words, when I tap a ViewCell to expand it, I want all the other ViewCells in the ListView to set their respective Expander control's "IsExpanded" property to false.

I have tried calling the "ItemTapped" event in the ListView and then cycling through the elements in the ListView - I'm not sure if this is the right approach?

What is the best way to access the properties of the Expander control within the ViewCell?

I'm not sure how to go about this - any help would be much appreciated.

Thanks.

DR_Bart
  • 33
  • 9
  • What happens when you "cycle thru the elements in the ListView"? Did you succeed in setting the "IsExpanded" properties to false? But it didn't actually collapse them? – ToolmakerSteve Apr 15 '21 at 02:01

1 Answers1

1

Updated

Edited to provide a pure UI solution, inspiration from get cells from ListView (or can try this one highlight the selected item)

Two steps:

  1. Define the ViewCell together with ListView in XAML.
<ListView x:Name="MyList" ...>
    <ListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <xct:Expander Tapped="Expander_Tapped" ...>
  1. Define the method in XAML.cs:
        private void Expander_Tapped(object sender, EventArgs e)
        {
            var expander = sender as Expander;
            var state = expander.IsExpanded;

            var cells = MyList.GetType().GetRuntimeProperties()
                .FirstOrDefault(info => info.Name == "TemplatedItems")?.GetValue(MyList);
            if (cells != null) { 
                foreach (ViewCell cell in cells as ITemplatedItemsList<Cell>)
                {
                    if (cell.BindingContext != null)
                    {
                        var child = cell.View as Expander;
                        child.IsExpanded = false;
                    }
                }
            }

            expander.IsExpanded = state;
        }

Previous answer:

Rather than manipulate the view property, I would prefer to do it in the model class.

Three steps here:

  1. Add a boolean property in your model class.
 public class YourDeviceModel : INotifyPropertyChanged
    {
        private bool flag;

        public bool Flag
        {
            get => flag;
            set
            {
                flag = value;
                OnPropertyChanged("Flag");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string name)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
  1. Define a TapCommand in your ViewModel when clicking (if using MVVM)
    public ICommand TapCommand => new Command<YourDeviceModel>(OnItemTapped);

    private void OnItemTapped(YourDeviceModel currentSelection)
    {
        //if your Devices is a ObservableCollection, convert it and reset all flags
        Devices.ToList().ForEach(x => x.Flag = false);
        //set the current one to true
        currentSelection.Flag = true;
    }
  1. Add property and command bindings in Expander (remove ItemTapped in ListView)
    <xct:Expander
        IsExpanded="{Binding Flag, Mode=TwoWay}"
        Command="{Binding Source={RelativeSource AncestorType={x:Type ViewModel:PageViewModel}, Mode=FindAncestorBindingContext}, Path=TapCommand}"
        CommandParameter="{Binding .}">
Shaw
  • 497
  • 5
  • 11
  • 1
    This may or may not be a good idea. IsExpanded might not have any meaning in the Model domain; it may purely be part of view state. (FYI: This is part of a larger problem with Data Binding; its great at simple mapping from a model, but not great once view needs are more complex. So either one does what you do, which runs counter to the goal of separating view from model, or one needs a "viewmodel" of each item, which is clumsy/error-prone, because now there are two heirarchies to keep in sync. I suspect that Data Binding will eventually be abandoned. Maybe MVU, but I haven't researched yet.) – ToolmakerSteve Apr 15 '21 at 02:12
  • I agree with keeping the logic in view only for most of the times (exceptions like the list needs to expand some items based on the data) and updated a solution without touching ViewModel. – Shaw Apr 15 '21 at 04:37
  • Unfortunately I can't use this solution for various reasons. I need the data template selector to choose different view cells. More Importantly I would also like to keep it MVVM as much as possible. Having an expanded state in the model would break this. I've tried the first approach and couldn't get it working because casting the ViewCell.View to a stackpanel always throws a null exception - not sure why. I'd have thought there would be a relatively straight forward way to do this that adheres to MVVM, but obviously not :/ However I do appreciate the effort of the answer! – DR_Bart Apr 15 '21 at 20:54
  • My bad, I was also testing Tapped event on the ViewCell and added a StackLayout there and code the logic. Just `var child = cell.View as Expander` would be fine, also updated the answer. – Shaw Apr 15 '21 at 21:11
  • Hi @DR_Bart, have you tried the updated code (one line changed). – Shaw Apr 19 '21 at 03:33