Search code examples
mauimaui-community-toolkitmaui-collectionview

Troubleshooting nested CollectionViews in .NET Maui and changing DataType


.net Maui CollectionViews I am trying to nest my views and set my DataType value to the respective ObservableCollections that have my objects I need to pull data from to display. Unfortunately I can't seem to get my DataType="model:Relapse" to change to anything else after it is set breaking my ability to have any other CollectionViews contained within.

IDE: Visual Studio 2022 PlatformDetails: net7.0-android33.0

Sample of XAML:

<CollectionView ItemsSource="{Binding RelapseDiaryEntries}" Margin="5" Grid.Row="0" VerticalScrollBarVisibility="Default">
                    <CollectionView.Header>
                        <StackLayout>
                            <Label Text="Entries:" Grid.Row="0" Margin="5,0,0,10" Padding="0" FontSize="20" FontAttributes="Bold"/>
                        </StackLayout>
                    </CollectionView.Header>
                    <CollectionView.EmptyView>
                        <Grid RowDefinitions="*,*">
                            <Label Text="Add a Relapse Diary Entry to get started!"/>
                        </Grid>
                    </CollectionView.EmptyView>
                    <CollectionView.ItemTemplate>
                        <DataTemplate x:DataType="viewmodel:RelapseDiaryViewModel">
                            <ContentView Padding="0" Margin="0">
                                <Frame Margin="0" Padding="0,10">
                                    <toolkit:Expander>
                                        <toolkit:Expander.Header>
                                            <Grid ColumnDefinitions="*,10,*" Margin="0" Padding="0" x:DataType="model:Relapse" >
                                                <Label Text="{Binding DateAndTime}" Grid.Column="0" Grid.Row="0" HorizontalOptions="End" VerticalOptions="Center" Margin="5" Padding="0" FontSize="16"/>
                                                <Label Text="|" FontSize="Title" Grid.Column="1" Grid.Row="0" HorizontalOptions="Center" VerticalOptions="Start" Padding="0,0,0,5" Margin="0" />
                                                <Label  Text="{Binding Location}" Grid.Column="2" Grid.Row="0" HorizontalOptions="Start" VerticalOptions="Center" Margin="5" Padding="0" FontSize="16"/>
                                            </Grid>
                                        </toolkit:Expander.Header>
                                        <StackLayout HeightRequest="300" x:DataType="{x:Null}">
                                            <Frame Padding="0" Margin="5" BackgroundColor="Black">
                                                <StackLayout x:DataType="viewmodel:RelapseDiaryViewModel" BindableLayout.ItemsSource="{Binding TriggersForUser}" HeightRequest="135" Background="black" Margin="0" Padding="0">
                                                    <BindableLayout.ItemTemplate>
                                                        <DataTemplate x:DataType="model:Trigger">
                                                            <Grid x:Name="Triggers" Margin="5" RowDefinitions="*,*" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" Padding="0" BackgroundColor="Red">
                                                                <Label Text="Triggers:" TextColor="AliceBlue" HorizontalOptions="CenterAndExpand" VerticalOptions="CenterAndExpand" Margin="0" Padding="0" Grid.Row="0"/>
                                                                <Frame Grid.Row="0" Margin="0" Padding="0" BackgroundColor="Pink">
                                                                    <Grid RowDefinitions="*,*">
                                                                        <toolkit:Expander>
                                                                            <toolkit:Expander.Header>
                                                                                <Label Text="{Binding Title}" Grid.Row="0"/>
                                                                            </toolkit:Expander.Header>
                                                                            <Label Text="{Binding Description}" Grid.Row="1"/>
                                                                        </toolkit:Expander>
                                                                    </Grid>
                                                                </Frame>
                                                            </Grid>
                                                        </DataTemplate>
                                                    </BindableLayout.ItemTemplate>
                                                </StackLayout>
                                            </Frame>
                                            <Frame Padding="0" Margin="5" BackgroundColor="Black">
                                                <StackLayout BindableLayout.ItemsSource="{Binding SymptomsForUser}" HeightRequest="135" Background="black" Margin="3">
                                                    <BindableLayout.ItemTemplate>
                                                        <DataTemplate x:DataType="model:Symptom">
                                                            <Grid x:Name="Symptoms" Margin="5" RowDefinitions="*,*">
                                                                <Label Text="Symptoms:" Grid.Row="0" TextColor="AliceBlue"/>
                                                                <Frame BorderColor="White" Grid.Row="1">
                                                                    <Grid RowDefinitions="*,*">
                                                                        <toolkit:Expander>
                                                                            <toolkit:Expander.Header>
                                                                                <Label Text="{Binding Title}" Grid.Row="0" TextColor="White"/>
                                                                            </toolkit:Expander.Header>
                                                                            <Label Text="{Binding Description}" Grid.Row="1" TextColor="White"/>
                                                                        </toolkit:Expander>
                                                                    </Grid>
                                                                </Frame>
                                                            </Grid>
                                                        </DataTemplate>
                                                    </BindableLayout.ItemTemplate>
                                                </StackLayout>
                                            </Frame>
                                        </StackLayout>
                                    </toolkit:Expander>
                                </Frame>
                            </ContentView>
                        </DataTemplate>
                    </CollectionView.ItemTemplate>
                </CollectionView>

Sample of my ViewModel

ObservableCollection<Relapse> _relapseDiaryEntries = new ObservableCollection<Relapse>();
        public ObservableCollection<Relapse> RelapseDiaryEntries
        {
            get { return _relapseDiaryEntries; }
            set { SetProperty(ref _relapseDiaryEntries, value); }
        }

        ObservableCollection<Model.Trigger> _triggersForUser = new ObservableCollection<Model.Trigger>();
        public ObservableCollection<Model.Trigger> TriggersForUser
        {
            get { return _triggersForUser; }
            set { SetProperty(ref _triggersForUser, value); }
        }

        ObservableCollection<Symptom> _symptomsForUser = new ObservableCollection<Symptom>();
        public ObservableCollection<Symptom> SymptomsForUser
        {
            get { return _symptomsForUser; } 
            set { SetProperty(ref _symptomsForUser, value); }
        }

        [RelayCommand]
        public async Task Reload()
        {
            PageTitle = "Relapse Diary";
            TheUser = ThisUser;
            RelapseDiaryEntries.Clear();
            await Init();
        }

        public async Task Init()
        {
            await LoadRelapses(TheUser.UserId);
        }

        private async Task LoadRelapses(int UserId)
        {
            ObservableCollection<Relapse> _allRelapses = await RelapseCalls.GetRelapsesAsync();
            ObservableCollection<Model.Trigger> _triggersAll = await TriggerCalls.GetTriggersAsync();
            ObservableCollection<Symptom> _symptomsAll = await SymptomCalls.GetSymptomsAsync();

            foreach (Relapse r in _allRelapses)
            {
                if (r.UserId == UserId)
                {
                    RelapseDiaryEntries.Add(r);
                }
            }

            try
            {
                foreach (Relapse r in RelapseDiaryEntries)
                {
                    foreach (Model.Trigger t in _triggersAll)
                    {
                        if (r.TriggerCollectionId == t.TriggerCollectionId)
                        {
                            TriggersForUser.Add(t);
                        }
                    }
                    foreach (Symptom s in _symptomsAll)
                    {
                        if (r.SymptomCollectionId == s.SymptomCollectionId)
                        {
                            SymptomsForUser.Add(s);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
                throw;
            }

        }

This gives me the Error: 'TriggersForUser' property not found on 'WGU_Capstone_C868.Model.Relapse', target property: 'Microsoft.Maui.Controls.StackLayout.ItemsSource' once compiled and running.

What I am trying to accomplish is a list of expandable items that then show a list of related items, that then expand to show a text description of those items.

I am doing MVVM and have tried a number of work arounds found online such as using a StackView inside a CollectionView with different DataTemplates set with the correct DataType for the data they hold.

Some ideas I have about what is going on is maybe the compiler is only seeing the first DataType and this is a compiler bug? Or after searching and reding all the documentation and even selling my soul to ChatGPT to find a solution I have repeatedly missunderstood how DataType works? Here is a sample of one of my attemps and the Error I am getting trying to bind objects.


Solution

  • "Since the prop isn't a part of that model it has locked it's sights on, it ignores my Path for the Prop and tells me that it isn't an actual thing."

    Because you haven't given it enough information to find the start of the Path.

    NOTE: If having trouble getting these to work, temporarily remove ALL x:DataType statements from the XAML file. They help Intellisense, and runtime-performance, but if you use ANY in a file, then you have to correctly set them at ALL nested places. (Hopefully this eventually won't be needed; there is no valid reason for XAML compiler to think that an ItemTemplate defaults to the same x:DataType as the containing collection. That makes no logical sense.) Get it to work without any x:DataType. (There may be Intellisense errors; disregard those, run it and see if there are any runtime errors.) Get it to work, then add them back in, if you want. (I often omit them.)

    I'll call your ViewModel class MyViewModel (I don't see a name for it in your question).

    Setting x:DataType="MyViewModel" does not tell Maui how to find the current instance of MyViewModel. I'll call the current page class MyPage. MyPage's BindingContext is the desired MyViewModel instance.

    Here are several ways to reach the desired Path.


    1. Give page an x:Name, and use a path from that:
    <ContentPage ...
        x:Name="thisPage"
        Class="MyPage">
        ...
          BindableLayout.ItemsSource=
            "{Binding VM.TriggersForUser, Source={x:Reference thisPage}}"
    

    This looks at x:Names in the page's XAML. It finds that MyPage is named thisPage.

    Where MyPage code-behind contains property VM (to get the correct type automatically):

    public partial class MyPage : ContentPage
    {
        public MyViewModel VM => (MyViewModel)BindingContext;
    }
    

    1. Search ancestors for type MyPage:
    <ContentPage ...
        xmlns:local="clr-namespace:MyMauiApp"   <-- replace with namespace that contains MyPage class.
        Class="MyPage">
    
          BindableLayout.ItemsSource=
            "{Binding VM.TriggersForUser, Source={RelativeReference 
              AncestorType={x:Type local:MyPage}}}"
    

    This searches up the XAML hierarchy. When it reaches MyPage, that is a matching type, so it starts the path there.

    Again, this relies on a code-behind property VM.


    1. Search type that is a BindingContext of some ancestor:
    <ContentPage ...
        xmlns:myvms="clr-namespace:MyMauiApp"   <-- namespace containing MyViewModel class.
        Class="MyPage">
    
          BindableLayout.ItemsSource=
            "{Binding TriggersForUser, Source={RelativeReference 
              AncestorType={x:Type myvms:MyViewModel}}}"
    

    This searches up the XAML hierarchy. When it reaches MyPage, it checks type of BindingContext, and sees that it matches MyViewModel.

    I've never done it this way -- referring directly to a ViewModel instead of to a UI class such as MyPage -- but I've seen others who have, so I assume it works now.