Search code examples
.netmaui

How to Bind Data Inside a DataTemplate and Control Read-Only State in .NET MAUI with Multiple ViewModels?


I am working on a .NET MAUI application where I have combined three ViewModels (ProjectDetailsViewModel, TODO_ViewModel, and TaskMaViewModel) into one MainPageViewModels class to display them on a single page. I’m using the CommunityToolkit.Mvvm.ComponentModel and CommunityToolkit.Mvvm.Input libraries to manage my ViewModels.

In my XAML, I am displaying a list of tasks, and I want to control the IsEnabled state of some Entry fields based on a property in TaskMaViewModel. I’m also trying to show a list of projects using a Picker inside the DataTemplate. However, I’m running into the following issues:

Issues:

Binding Read-Only State: I am trying to bind the IsEnabled property of an Entry to a boolean property (IsEditable) in TaskMaViewModel, but it doesn't seem to work. The fields are not respecting the IsEnabled binding.

Using Multiple ViewModels in a DataTemplate: I want to include another ViewModel (ProjectDetailsViewModel) inside the DataTemplate to display a list of projects in a Picker. How can I correctly bind the Picker to this ViewModel and display the list of projects?

Questions:

How can I correctly bind data inside a DataTemplate in .NET MAUI, particularly when trying to control the IsEnabled state of an Entry field? Is there something wrong with how I’m using RelativeSource or the binding path? How can I include another ViewModel (ProjectDetailsViewModel) inside the DataTemplate to bind the Picker control to a list of projects? What’s the correct approach to achieve this in .NET MAUI? Any guidance would be greatly appreciated! Thank you.

public partial class MainPageViewModels : ObservableObject
    {
        [ObservableProperty]
        private bool isToDoListVisible;

        public bool isTaskListVisible => !isToDoListVisible;

        private string _btnColor = "#512BD4";
        public string BtnColor
        {
            get => _btnColor;
            set => SetProperty(ref _btnColor, value);
        }

        private string _btnText = "To-Do";

        public string BtnText
        {
            get => _btnText;
            set => SetProperty(ref _btnText, value);
        }

        public ProjectDetailsViewModel projectvm { get; set; }
        public TODO_ViewModel todovm { get; set; }
        public TaskMaViewModel taskvm { get; set; }

        public MainPageViewModels()
        {
            projectvm = new ProjectDetailsViewModel();
            todovm = new TODO_ViewModel();
            taskvm = new TaskMaViewModel(projectvm);
            isToDoListVisible = false;
        }

        [RelayCommand]
        private void ToggleToDoList()
        {
            IsToDoListVisible = !IsToDoListVisible;
        }

        partial void OnIsToDoListVisibleChanged(bool value)
        {
            if (isTaskListVisible)
            {
                BtnColor = "#512BD4";
                BtnText = "To-Do";
            }
            else
            {
                BtnColor = "#ADD8E6";
                BtnText = "Task List";
            }
            OnPropertyChanged(nameof(isTaskListVisible));
        }
    }

Below is the code snippet I’m working with:

<StackLayout Grid.Row="2" IsVisible="{Binding isTaskListVisible}">

    <!-- Header Row -->
    <Grid Padding="10" BackgroundColor="#f0f0f0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Label Text="ID" Grid.Column="0" FontAttributes="Bold" HorizontalTextAlignment="Center"/>
        <Label Text="Task Name" Grid.Column="1" FontAttributes="Bold" HorizontalTextAlignment="Center"/>
        <Label Text="Start Time" Grid.Column="2" FontAttributes="Bold" HorizontalTextAlignment="Center"/>
        <Label Text="End Time" Grid.Column="3" FontAttributes="Bold" HorizontalTextAlignment="Center"/>
        <Label Text="Time Taken" Grid.Column="4" FontAttributes="Bold" HorizontalTextAlignment="Center"/>
        <Label Text="Project Name" Grid.Column="5" FontAttributes="Bold" HorizontalTextAlignment="Center"/>
    </Grid>

    <ScrollView HeightRequest="400">
        <ListView ItemsSource="{Binding taskvm.Tasks}" SelectionMode="Single">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <SwipeView>
                            <SwipeView.LeftItems>
                                <SwipeItems>
                                    <SwipeItem Text="Edit"
                                        BackgroundColor="LightBlue"
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type ContentPage}}, Path=BindingContext.taskvm.DeleteTaskCommand}"
                                        CommandParameter="{Binding Source={RelativeSource AncestorType={x:Type xs:TaskMang}}, Path=id}" />
                                    <SwipeItem Text="Delete"
                                        BackgroundColor="LightCoral"
                                        CommandParameter="{Binding .}" />
                                </SwipeItems>
                            </SwipeView.LeftItems>

                            <SwipeView.Content>
                                <Grid Padding="10">
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="*" />
                                        <ColumnDefinition Width="*" />
                                        <ColumnDefinition Width="*" />
                                        <ColumnDefinition Width="*" />
                                        <ColumnDefinition Width="*" />
                                    </Grid.ColumnDefinitions>

                                    <Entry Text="{Binding Source={RelativeSource AncestorType={x:Type xs:TaskMang}}, Path=id}" Grid.Column="0"  />
                                    <Entry Text="{Binding Source={RelativeSource AncestorType={x:Type xs:TaskMang}}, Path=TaskName}" Grid.Column="1" IsEnabled="{Binding Source={RelativeSource AncestorType={x:Type vm:TaskMaViewModel}}, Path=IsEditable}" />
                                    <Entry Text="{Binding Source={RelativeSource AncestorType={x:Type xs:TaskMang}}, Path=TaskStartTime}" Grid.Column="2" />
                                    <Entry Text="{Binding Source={RelativeSource AncestorType={x:Type xs:TaskMang}}, Path=TaskEndTime}" Grid.Column="3" />
                                    <Entry Text="{Binding Source={RelativeSource AncestorType={x:Type xs:TaskMang}}, Path=TaskTimeTaken}" Grid.Column="4" />
                                    <!--<Entry Text="{Binding Source={RelativeSource AncestorType={x:Type xs:TaskMang}}, Path=ProjectName}" Grid.Column="5"/>-->
                                    <Picker Title="Select Project" 
                                        WidthRequest="150" 
                                        ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type xs:ProjectModels}}, StringFormat='{0}'}" 
                                        ItemDisplayBinding="{Binding ProjectName}"
                                        SelectedItem="{Binding Source={RelativeSource AncestorType={x:Type xs:TaskMang}}, Path=ProjectName}"
                                        Grid.Column="5"
                                        FontSize="10"/>
                                </Grid>
                            </SwipeView.Content>
                        </SwipeView>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </ScrollView>

</StackLayout>

Solution

  • Update

    To answer your question of not fetching data when using CollectionView, I made a demo to share my solution.

    Background: we have a MainPage which contains a CollectionView and one MainPageViewModels in which we instantiate three other viewModels, projectvm, todovm and taskvm.

    First, I will determine the BindingContext of MainPage, so we set it in MainPage.cs,

    public MainPage()
    {
        InitializeComponent();
        this.BindingContext = new MainPageViewModels();
    }
    

    Please note that you don't set BindingContext or use any compiled bindings in XAML. If you really want to do that, please share the code.

    Then, we may focus on the ListView part. Let's say we define the Delete or Edit command in the taskvm.

    Here is the code for TaskMaViewModel. You may see that I create a collection of TaskMang and add some test data for debugging.

    public partial class TaskMaViewModel : ObservableObject
    {
        [ObservableProperty]
        ObservableCollection<TaskMang> tasks = new ObservableCollection<TaskMang>();
    
        [ObservableProperty]
        bool isEditable = false;
    
    
        [RelayCommand]
        public void DeleteTask(TaskMang taskMang)
        {
             Tasks.Remove(taskMang);
        }
    
        [RelayCommand]
        public void EditTask(TaskMang taskMang)
        {
            taskMang.TaskName = "I change it";
        }
    
    
        public TaskMaViewModel(ProjectDetailsViewModel projectvm)
        {
            // Add some test data for debugging
            Tasks.Add(new TaskMang { Id = "id1", TaskName = "TaskName1", TaskTimeTaken = "1H" });
            ...
        }
    }
    

    Then I need to consume it in XAML. For the SwipeItem, we should use the Relative Bindings to bind the Command property to EditTaskCommand/DeleteTaskCommand in taskvm. Please refer to the following code,

    <ScrollView HeightRequest="400">
        <ListView ItemsSource="{Binding taskvm.Tasks}" SelectionMode="Single">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <SwipeView>
                            <SwipeView.LeftItems>
                                <SwipeItems>
                                    <SwipeItem Text="Edit"
                                        BackgroundColor="LightBlue"
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type vm:MainPageViewModels}}, Path=taskvm.EditTaskCommand}"
                                        CommandParameter="{Binding .}" />
                                    <SwipeItem Text="Delete"
                                        BackgroundColor="LightCoral"
                                        Command="{Binding Source={RelativeSource AncestorType={x:Type vm:MainPageViewModels}}, Path=taskvm.DeleteTaskCommand}"
                                        CommandParameter="{Binding .}" />  
                                </SwipeItems>
                            </SwipeView.LeftItems>
    

    Okay let's see the Entry part. As I said before, you don't have to use RelativeBinding for the Entry. When setting the ItemsSource to taskvm.Tasks, then each ViewCell can directly access the Id or TaskName of each object in the collection. That means the BindingContext of each ViewCell is set to the corresponding Item in the Task collection.

    But IsEnabled property is different. It should first find taskvm and then find the IsEditable property in taskvm.

        <Entry Text="{Binding Id}" Grid.Column="0"  />
        <Entry Text="{Binding TaskName}" Grid.Column="1" IsEnabled="{Binding Source={RelativeSource AncestorType={x:Type vm:MainPageViewModels}}, Path=taskvm.IsEditable}" />
        <Entry Text="{Binding TaskStartTime}" Grid.Column="2" />
        <Entry Text="{Binding TaskEndTime}" Grid.Column="3" />
        <Entry Text="{Binding TaskTimeTaken}" Grid.Column="4" />
    

    This is the TaskMang,

    public partial class TaskMang : ObservableObject
    {
    
        [ObservableProperty]
        string id;
    
        [ObservableProperty]
        string taskName;
    
        [ObservableProperty]
        string taskTimeTaken;
    
    }
    

    enter image description here


    Below is the origin answer

    Binding Read-Only State

    Let's say you set the BindingContext for the MianPage,

        public MainPage()
        {
            InitializeComponent();
            this.BindingContext = new MainPageViewModels();
        }
    

    In MainPageViewModels, you define a TaskMaViewModel instance named taskvm. So if you want to use Bind to an ancestor, you may try this,

    <Entry .... IsEnabled="{Binding Source={RelativeSource AncestorType={x:Type vm:MainPageViewModels}}, Path=taskvm.IsEditable}" />
    

    In this way, IsEnabled Property will first find its parent's BindingContext MainPageViewModels and bind to taskvm.IsEditable property.

    By the way, is taskvm.Tasks a Collection of TaskMang object which contains id, TaskName and other properties? Then why not just use

    <Entry Text="{Binding id}" Grid.Column="0" />
    

    Binding Picker ItemSources

    Since you also define a ProjectDetailsViewModel instance called projectvm in MainPageViewModels, and you pass it into the TaskMaViewModel Constructor.

    public class TaskMaViewModel
    {
        public ProjectDetailsViewModel ProjectVM {  get; set; } 
        ...
    
        public TaskMaViewModel(ProjectDetailsViewModel projectvm)
        {
            this.ProjectVM = projectvm;
    

    And since we could get the TaskMaViewModel as BindingContext, we could also get ProjectVM.

    Let's say there is a PickerItems in ProjectVM, and we use some test code for debugging, like below,

    public partial class ProjectDetailsViewModel : ObservableObject
    {
        [ObservableProperty]
        ObservableCollection<PickerItem> pickerItems = new ObservableCollection<PickerItem>();
    
    
        public ProjectDetailsViewModel()
        {
            PickerItems.Add(new PickerItem() { ProjectId = 1, ProjectName = "project1" });
            PickerItems.Add(new PickerItem() { ProjectId = 2, ProjectName = "project2" });
            PickerItems.Add(new PickerItem() { ProjectId = 3, ProjectName = "project3" });
        }
    }
    
    public class PickerItem
    {
        public string ProjectName { get; set; }
        public int ProjectId { get; set; }
    }
    

    Now, we want to use data binding for Picker in DataTemplate,

    <Picker Title="Select Project" 
        WidthRequest="150" 
        ItemsSource="{Binding Source={RelativeSource AncestorType={x:Type vm:MainPageViewModel}}, Path=taskvm.ProjectVM.PickerItems}"
        ItemDisplayBinding="{Binding ProjectName}"
        Grid.Column="5"
        FontSize="10"/>
    

    For more info, you may refer to Populate a Picker with data using data binding


    Here is a screenshot,

    enter image description here