5
votes

I have a custom cell displayed in a listview. It should not be working but my concern is that it works and I don't understand why.

Let me lay things out for you, because it's a little bit complex.

Basically, I'm displaying a contact list with a search field on top. I have various other things to display that are out of the scope of this question, but you'll see them throughout the code for clarity, notably in xaml and in the Data template selector.

I'm using different types of custom cells to display each part of my contact list (there's a header cell, a search cell, and some more).

Here, ContactsPage is holding the listview and the declaration of the datatemplate.

<ContentPage>
    <ContentPage.Resources>
        <ResourceDictionary>


            <DataTemplate x:Key="HeaderTemplate">
                <ViewCell>
                    <StackLayout>
                        <local:HeaderView/>    
                    </StackLayout>
                </ViewCell>
            </DataTemplate>


            <DataTemplate x:Key="SearchTemplate">
                <local:SearchCell/>                 //<=== Important
            </DataTemplate>


            <DataTemplate x:Key="CategoryTemplate">
                <ViewCell
                    x:Name="CategoryCell">
                    <Label
                        Text="CategoryCell" ></Label>
                </ViewCell>
            </DataTemplate>


            <DataTemplate x:Key="SelectionTemplate">
                <ViewCell
                    x:Name="SelectionCell">
                    <Label
                        Text="Selection Cell" ></Label>
                </ViewCell>
            </DataTemplate>


            <DataTemplate x:Key="ContactTemplate">
                <ViewCell
                    x:Name="ContactCell">
                    <Label
                        Text="{Binding FirstName}" ></Label>
                </ViewCell>
            </DataTemplate>

            <local:ContactDataTemplateSelector x:Key="TemplateSelector"
                                              HeaderTemplate="{StaticResource HeaderTemplate}"
                                              SearchTemplate="{StaticResource SearchTemplate}"
                                              CategoryTemplate="{StaticResource CategoryTemplate}"
                                              SelectionTemplate="{StaticResource SelectionTemplate}"
                                              ContactTemplate="{StaticResource ContactTemplate}"/>
        </ResourceDictionary>
    </ContentPage.Resources>

You see I have various Datatemplates, each for its own use. Header is working, the rest is in progress, the only thing I care about is the Search implementation. From the cell, to the viewmodel, and through the data template.

Now this was only the resources, here's the actual page UI (only relevant code)

<ContentPage.Content>
   ... Content of the page, including the actual listview
            <ListView 
                x:Name="ContactsListView"
                HasUnevenRows="True""
                ItemTemplate="{StaticResource TemplateSelector}"
                ItemsSource="{Binding ListSource}">
            </ListView>
    </ContentPage.Content>

Let me take you through the journey of the logic behind this, in the viewmodel of that view. The code-behind of the view itself does nothing, and here is the ViewModel of that contact list.

public class ContactsViewModel : BaseViewModel, IContactsViewModel
    {
        readonly IContactsService _service;
        List<object> _listSource;

        public List<object> ListSource
        {
            get => _listSource;
            private set
            {
                _listSource = value; 
                OnPropertyChanged();
            }
        }

        public string CurrentText => "HelloX";             //<=== Important
        readonly ISearchViewModel _searchViewModel;
        readonly ICategoryFilterViewModel _categoryFilterViewModel;
        readonly ISelectionViewModel _selectionViewModel;

        public ContactsViewModel()
        {
            _service = new();

            HeaderViewModel = new HeaderViewModel();

            _searchViewModel = new();
            _categoryFilterViewModel = new();
            _selectionViewModel = new();

            ListSource = GenerateDefaultList();
        }

        public async Task LoadContacts()       //Called when UI appears.
        {
            await _service.LoadContacts();

            var list = GenerateDefaultList();
            list.AddRange(_service.Contacts);

            ListSource = list;
        }

        List<object> GenerateDefaultList()
        {
            return new List<object>()
            {
                HeaderViewModel,
                _searchViewModel,              //<===== important
                _categoryFilterViewModel,
                _selectionViewModel
            };
        }
    }

I'm including most of the code for the sake of clarity ; the important part here that I want to emphasize, is that the ListSource already has some viewmodels inside it. I'm using that source to populate my listview, and the type of object defines what type of data template i'm gonna use. This is done in the DataTemplate selector, which is here :

public class ContactDataTemplateSelector : DataTemplateSelector
    {
        public DataTemplate ContactTemplate { get; set; }
        public DataTemplate HeaderTemplate { get; set; }
        public DataTemplate SearchTemplate { get; set; }
        public DataTemplate CategoryTemplate { get; set; }
        public DataTemplate SelectionTemplate { get; set; }

        protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
        {            
            switch (item)
            {
                case HeaderViewModel _:
                    return HeaderTemplate;
                case SearchViewModel _:
                    return SearchTemplate;            //<==== important
                case CategoryFilterViewModel _:
                    return CategoryTemplate;
                case SelectionViewModel _:
                    return SelectionTemplate;
                default:
                    return ContactTemplate;
            }
        }
    }

So I do have an instance of the SearchViewModel (the only one of importance to my question), but its never said anywhere that the ViewCell of the search data template actually uses an SearchViewModel. I'm just using it as a condition to my if statement.

Here is the search cell that is used in the data template (which itself is chosen with the data template selector)

<ViewCell x:Class="MYNAMESPACE.SearchCell">
    <AbsoluteLayout>
        <Frame>
            <StackLayout>
                <Entry
                    Placeholder="{Binding PlaceHolderText}"/>
                <Button
                    Text="{Binding CurrentText}"
                    Command="{Binding SearchCommand}"/>
            </StackLayout>
        </Frame>
    </AbsoluteLayout>
</ViewCell>

I removed as much as I could without risking obscuring the context. I know this is a wall of code, but I believe it'll be useful if you decide to investigate.

From my understanding, I never provide a Binding Context to my custom ViewCell (the search cell). I do have bindings inside it, notably, my working example is the CurrentText. I have it as text in my SearchViewModel

public class SearchViewModel : ISearchViewModel
    {
        public string CurrentText => "<TODO SEARCH>";     //<=== Important

        //NotifyPropertyChanged Implementation
    }

I have another CurrentText in ContactsViewModel, but the text that is displayed at runtime is the one from SearchViewModel. I'm seeing "" and not "HelloX". This is what I want. I just do not understand how/why the cell uses my viewmodel.

I only use the viewmodel to chose which data template to display, nowhere that viewmodel is being set as the binding context of that data template, nor viewcell. Or am I ? Where is the binding context coming from?

1

1 Answers

4
votes

Thanks for the detailed question, I'm pretty sure I'm following what you've done, so hopefully this helps:

The BindingContext of every element on the page is set based on its parent's, if not otherwise specified. I'm sure you've probably seen that when you set the BindingContext for a page, it flows down to all elements of that page unless you override it explicitly. So, for lists, the BindingContext of each item is set automatically to its corresponding ItemsSource object. So when you return the SearchTemplate from your template selector, all of its elements will inherit the binding context of that item, which in this case is the instance of SearchViewModel that you created and put in the ListSource during GenerateDefaultList.