0

I have set up a new MAUI application and started to structure the application for the MVVM Pattern with a folder for Views, Viewmodels and Models. The root folder only containing the App.xaml and AppShell.xaml and the MauiProgram.cs.

I have setup the navigation in the shell and am able to navigate to the different views. One of my views is basically a list with a hovering button in the bottom right corner. When i click this button I show a Community Toolkit popup containing a textinput and a button. When I press this button I want to add an Item to a list.

So I set a View "DailyPage" and a Viewmodel "DailyViewModel". The Viewmodel code looks like this:

public class DailyViewModel : ViewModelBase
    {
        ObservableRangeCollection<ExampleModel> _DailyList { get; set;}
        public ObservableRangeCollection<ExampleModel> DailyList
        {
            get => _DailyList;
            set
            {
                _DailyList = value;
            }
        }
        public AsyncCommand RefreshCommand { get; }

        public DailyViewModel()
        {
            Title = "Daily Page";
            AddObjectCommand= new MvvmHelpers.Commands.Command(AddObject);
            RefreshCommand = new AsyncCommand(Refresh);
            _DailyList = new ObservableRangeCollection<ExampleModel>
            {
                new ExampleModel{ Name = "Test1" },
                new ExampleModel{ Name = "Test2" },
                new ExampleModel{ Name = "Test3" },
                new ExampleModel{ Name = "Test4" }
            };
        }
        string addName = "-";
        public string AddName
        {
            get => addName;
            set => SetProperty(ref addName, value);
        }

        public ICommand AddObjectCommand { get; }

        void AddObject()
        {
            ExampleModel ex = new ExampleModel() { Name = AddName };
            _DailyList.Add(ex);
        }

The DailyPage view looks like this:

<AbsoluteLayout>
        <ListView BackgroundColor="Transparent"
                  AbsoluteLayout.LayoutBounds="0,0,1,1"
                  AbsoluteLayout.LayoutFlags="PositionProportional,HeightProportional,WidthProportional"
                  Margin="10"
                  ItemsSource="{Binding DailyList,Mode=TwoWay}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="model:ExampleModel">
                    <ViewCell>
                        <Grid Padding="5"
                              HorizontalOptions="Center">
                            <Label Text="{Binding Name}"
                                   FontSize="Large"
                                   VerticalOptions="Center"
                                   HorizontalOptions="Center"
                                   Grid.Column="1"/>
                        </Grid>
                       
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Button Text="+"
                FontSize="Large"
                FontAttributes="Bold"
                AbsoluteLayout.LayoutBounds="1,1,100,100"
                AbsoluteLayout.LayoutFlags="PositionProportional"
                Margin="10"
                Clicked="onAddEntryClicked"/>
    </AbsoluteLayout>

The Entry popup where the data shall be entered looks like this:

<toolkit:Popup.BindingContext>
        <viewmodels:DailyViewModel/>
</toolkit:Popup.BindingContext>
    
    <VerticalStackLayout>
        <Label Text="Name"
               FontSize="Large"/>
        <Entry Text="{Binding AddName}"/>
        <Button Command="{Binding AddObjectCommand}"
                Text="Add"/>
    </VerticalStackLayout>

I also have referenced to the DailyViewModel in both the DailyPage and the AddEntryPopup like this:

x:DataType="viewmodels:DailyViewModel"

so the Compiled DataBinding and the Intellisense works as intended.

Up until the adding of data via the command everything works as expected. I navigate to the view, I see a list with the example data I added to the list in the viewmodel. When I press the "+" Button the popup shows and lets me enter the data. When the "Add" button is pressed even the "AddObjectCommand" is executed and when I debug it and look in the watchlist the object is successfully added.

I was expecting, that when I add an Object to the ObservableRangeCollection, this object will later be found in my List as I set up a databinding to that observableRangeCollection. But I found that everytime I execute the AddObjectCommand, the Collection only contains of the items that I set up as demo data (Test1, Test2, Test3, Test4) but not the ones I already added.

Why is this data lost even though the actual view is not changed? How would you store data so it isnt lost when you change views?

Initially I thought the problem will occur when I start switching views, as data is not stored globally in an MVVM pattern, but it seems that data is also not retained when working with a viewmodel. This is my first time really trying to use the MVVM patterns. I have read a lot about it and I think I got the structure and the goal, but I seem to fail on the execution. Edit: DailyPage Codebehind

public partial class DailyPage : ContentPage
{
    public DailyPage()
    {
        InitializeComponent(); 
    }
    async void onAddEntryClicked(object sender, EventArgs e)
    {
        var entryPopup = new AddEntryPopup();
        await this.ShowPopupAsync(entryPopup);
    }
}
5
  • 2
    your popup is creating a new instance of the VM, not sharing the same instance with the other page Commented Apr 24, 2023 at 15:19
  • Use the debugger, set a breakpoint at the constructor of the DailyViewModel and now you should see why Commented Apr 24, 2023 at 15:51
  • Please show the code behind. That's where you should set the BindingContext in your particular scenario. Also, please show how you instantiate the popup. Currently, you're creating a new instance of the ViewModel everytime, because of this line: <viewmodels:DailyViewModel/>. You should reuse the ViewModel instead of instantiating it with every new popup. Commented Apr 25, 2023 at 9:47
  • @ewerspej For now i went with Jessie Zhangs solution, but initially I also wanted to use the DailyViewModel for the popup, as it logically belongs to the DailyPage anyways. How would you reuse or reference to an instance of a viewmodel when calling a popup? Commented Apr 27, 2023 at 19:17
  • @NicoSz It depends. With Shell, I would use MAUI's built-in dependency injection capabilities and make the ViewModel a singleton instance in the IoC/services container. That way, all Views that share that particular ViewModel will work on the same instance and have access to the same data. Commented Apr 27, 2023 at 19:25

1 Answer 1

0

Even the two pages (DailyPage and AddEntryPopup) share the same Viewmodel (DailyViewModel.cs),two different new instances of the DailyViewModel.cs will be created. So the two pages cannot share the same value for _DailyList and variable public string AddName.

For this problem, you can use two different VMs for the two pages and pass the new added data between the pages(for example, you can use MessageCenter to pass data).

I have created a demo and achieved this function. You can refer to the following code:

MainPage.xaml

<ContentPage.BindingContext>
    <viewmodels:DailyViewModel></viewmodels:DailyViewModel>
</ContentPage.BindingContext>

 <VerticalStackLayout
        Spacing="25"
        Padding="30,0"
        VerticalOptions="Start">
     
            <ListView BackgroundColor="Transparent"
              Margin="10"
              ItemsSource="{Binding DailyList,Mode=TwoWay}">
                <ListView.ItemTemplate>
                    <DataTemplate x:DataType="models:ExampleModel">
                        <ViewCell>
                            <Grid Padding="5"
                          HorizontalOptions="Center">
                                <Label Text="{Binding Name}"
                               FontSize="Large"
                               VerticalOptions="Center"
                               HorizontalOptions="Center"
                               Grid.Column="1"/>
                            </Grid>

                        </ViewCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
            <Button Text="+"
            FontSize="Large"
            FontAttributes="Bold"
            AbsoluteLayout.LayoutBounds="1,1,100,100"
            AbsoluteLayout.LayoutFlags="PositionProportional"
            Margin="10"
            Clicked="onAddEntryClicked"/>
 </VerticalStackLayout>

MainPage.xaml.cs

public partial class MainPage : ContentPage 
{
      public MainPage()
      {
            InitializeComponent();
      }

    private async  void onAddEntryClicked(object sender, EventArgs e)
    {
        var addPage = new AddItemPage();
        await Navigation.PushModalAsync(addPage);
    }
}

DailyViewModel.cs

public class DailyViewModel: INotifyPropertyChanged 
{
    ObservableCollection<ExampleModel> _DailyList { get; set; }
    public ObservableCollection<ExampleModel> DailyList
    {
        get => _DailyList;
        set
        {
            _DailyList = value;
        }
    }
    public ICommand RefreshCommand { get; }

    public DailyViewModel()
    {
        _DailyList = new ObservableCollection<ExampleModel>
        {
            new ExampleModel{ Name = "Test1" },
            new ExampleModel{ Name = "Test2" },
            new ExampleModel{ Name = "Test3" },
            new ExampleModel{ Name = "Test4" }
        };


        MessagingCenter.Subscribe<AddItemViewModel, ExampleModel>(this, "NewItem", (obj, item) =>
        {
            var newItem = item as ExampleModel;

            _DailyList.Add(newItem);
        });


    }

    bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (Object.Equals(storage, value))
            return false;

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;

}

AddItemPage.xaml

<?xml version="1.0" encoding="utf-8" ?> 
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiAddItemToListApp.Views.AddItemPage"
             xmlns:viewmodels="clr-namespace:MauiAddItemToListApp.ViewModels"
             Title="AddItemPage">

    <VerticalStackLayout>
        <Label Text="Name"
               FontSize="Large"/>
        <Entry Text="{Binding AddName}"/>
        <Button Command="{Binding AddObjectCommand}"
                Text="Add"/>
    </VerticalStackLayout>
</ContentPage>

AddItemPage.xaml.cs

public partial class AddItemPage : ContentPage 
{
      public AddItemPage()
      {
            InitializeComponent();

            this.BindingContext =  new AddItemViewModel(Navigation);
      }
}

AddItemViewModel.cs

public class AddItemViewModel: INotifyPropertyChanged 
{
    public INavigation Navigation { get; set; }

    public AddItemViewModel(INavigation navigation)
    {
        AddObjectCommand = new Command(addItem);
        this.Navigation = navigation;
    }

    string addName = "";
    public string AddName
    {
        get => addName;
        set => SetProperty(ref addName, value);
    }

    public ICommand AddObjectCommand { get; }

    private async void addItem()
    {
        ExampleModel newItem = new ExampleModel();
        newItem.Name = AddName;
        //send message
        MessagingCenter.Send(this, "NewItem", newItem);

        await Navigation.PopModalAsync();
    }

    bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (Object.Equals(storage, value))
            return false;

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}
Sign up to request clarification or add additional context in comments.

2 Comments

I think it should be possible to just reuse the ViewModel, but that requires some additional information from the OP.
Thanks for your answer, I tried implementing your solution but VS advised me to use WeakReferenceMessenger Instead of MessagingCenter as its obsolete. After changing over to that the application works as intended!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.