.Net Maui MVVM Navigate and Pass Object Between Views

Viewed 77

I am struggling to pass an object from one view to another using .Net Maui and a UI independent MVVM pattern.

Most or all MAUI examples I have found to date use and promote MVVM. They also tend to store the View-Models within the UI project and utilize MAUI App Shell navigation directly. This in my opinion and understanding potentially omits some of the benefits of MVVM as View Models would not work with other UI projects.

I have attempted to create a working example* with the View Models, Models and Services being in a separate UI independent project and referenced by the UI project(s) which contain the Views. Sample Project Published to GitHub I would like to update the project with a solution so it can be a working example.

Project Layout

I am specifically stuck where when clicking on a single object from a list of objects it opens a detail view but the object is not passed successfully. In this case my example utilizes a list of customer orders and I am attempting to open the order in a detail view when clicking on an order from the order list.

My problem lies somewhere between the XAML binding to the GoToOrdersCommand, how the command is implemented, and Order object passed to the Order View-Model.

Why do I have both a DelegateCommand and RelayCommand, I am not sure. Different examples I have viewed have used these names, I am not sure if one name is correct. Or if they should be combined into one class. I believe the only significant difference for this example is that RelayCommand accepts an object as a parameter.

Orders View-Model

    namespace Orders.Common.ViewModel
    {
        public class OrdersViewModel : ViewModelBase
        {
            public ObservableCollection<Order> Orders { get; } = new();
    
            private readonly OrderDataProvider _orderDataProvider;
            private IOrderNavigation _navigationService;
    
            public RelayCommand<Order> GoToOrdersCommand { get; }
    
            private Order _order;
    
            public OrdersViewModel(IOrderNavigation navigationService)
            {
                Orders = new ObservableCollection<Order>();
                _orderDataProvider = new OrderDataProvider();
                _navigationService = navigationService;
    
                _order = new Order();
    
                GoToOrdersCommand = new RelayCommand<Order>((order) => OrderDetails(_order));
    
                Load();
            }
    
            public void Load()
            {
                var orders = _orderDataProvider.GetAllOrders();
                Orders.Clear();
    
                foreach (var order in orders)
                    Orders.Add(order);
            }
    
            public void OrderDetails(Order order)
            {
                if (order == null)
                    return;
    
                _navigationService.NavigateToOrderAsync(order);
            }
        }
    }

Order View-Model

public class OrderViewModel : ViewModelBase
    {
        private Order _order;

        public Order Order
        {
            get => _order;
            set
            {
                if (_order != value)
                {
                    _order = value;
                    RaisePropertyChanged();
                }
            }
        }

        private OrderDataProvider _orderDataProvider = new OrderDataProvider();

        public DelegateCommand SaveCommand { get; }

        public ICommand SaveOrder { get; set; }


        public OrderViewModel()
        {
            _order = new Order() { OrderID = -1, Customer = "", OrderDate = DateTime.Now };

            SaveCommand = new DelegateCommand(Save, () => CanSave);
        }

        public OrderViewModel(Order order)
        {
            _order = order;

            SaveCommand = new DelegateCommand(Save, () => CanSave);
        }

        public bool CanSave => !string.IsNullOrEmpty(CustomerName) && CustomerName.Length >= 3;

        public void Save()
        {
            throw new NotImplementedException();
        }

        public int OrderID
        {
            get => _order.OrderID;
            set
            {
                if (_order.OrderID != value)
                {
                    _order.OrderID = value;
                    RaisePropertyChanged();
                }
            }
        }

        public string CustomerName
        {
            get => _order.Customer;
            set
            {
                if (_order.Customer != value)
                {
                    _order.Customer = value;
                    RaisePropertyChanged();
                    RaisePropertyChanged(nameof(CanSave));
                    SaveCommand.RaiseCanExecuteChanged();
                }
            }
        }

        public DateTime OrderDate
        {
            get => _order.OrderDate;
            set
            {
                if (_order.OrderDate != value)
                {
                    _order.OrderDate = value;
                    RaisePropertyChanged();
                }
            }
        }
    }

Inherited UI Specific Navigation

    namespace TestOrders.Navigate
    {
        public class OrderNavigationService : IOrderNavigation
        {
            public void NavigateToOrdersAsync()
            {
                Shell.Current.GoToAsync(nameof(OrdersPage));
            }
    
            public void NavigateToOrderAsync(Order order)
            {
                Shell.Current.GoToAsync(nameof(OrderPage), true, new Dictionary<string, object> { { "Order", order } });
            }        
        }
    }

RelayCommand

namespace Orders.Common.ViewModel.Command
{
    public class RelayCommand<T> : ICommand
    {
        private Action<object> _execute;
        private Func<object, bool> _canExecute;

        public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
        {
            _execute = execute ?? throw new ArgumentNullException(nameof(execute));
            _canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged;

        public void RaiseCanExecuteChanged()
        {
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null || _canExecute(parameter);
        }

        public void Execute(object parameter)
        {
            _execute(parameter);
        }
    }
}

Orders View

<?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="TestOrders.Pages.OrdersPage"
             Title="OrdersPage"
             xmlns:model="clr-namespace:Orders.Common.Model;assembly=Orders.Common"
             xmlns:viewmodel="clr-namespace:Orders.Common.ViewModel;assembly=Orders.Common"
             x:DataType="viewmodel:OrdersViewModel">

        <VerticalStackLayout>
            <Label Text="Orders" HorizontalOptions="Center"/>

            <CollectionView
                    ItemsSource="{Binding Orders}"
                    SelectionMode="None">
                <CollectionView.ItemTemplate>
                    <DataTemplate x:DataType="model:Order">
                        <Grid Padding="10">
                            <Frame HeightRequest="70">
                                <Frame.GestureRecognizers>
                                    <TapGestureRecognizer 
                                            Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:OrdersViewModel}}, Path=GoToOrdersCommand}"
                                            CommandParameter="{Binding .}"/>
                                </Frame.GestureRecognizers>
                                
                                <Grid Padding="0" ColumnDefinitions="20,*">
                                    <Label Text="{Binding OrderID}" Grid.Column="0"/>
                                    <Label Text="{Binding Customer}" Grid.Column="1"/>
                            </Grid>
                            </Frame>
                        </Grid>
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>
        </VerticalStackLayout>
</ContentPage>

*Being new to .Net Maui, XAML based UI's, MVVM patterns and inexperienced with additional topics such as interfaces and delegates there are likely best practices or coding conventions I have missed. I am also trying to avoid utilizing the MVVM community toolkit or other toolkits until I have a better understanding of MVVM and how the interfaces and commands function.

2 Answers

I believe that this

GoToOrdersCommand = new RelayCommand<Order>((order) => OrderDetails(_order));

should be

GoToOrdersCommand = new RelayCommand<Order>((order) => OrderDetails(order));

_order is a new instance that you create on the preceding line, NOT the instance being passed in from the view's binding

Change to OrdersViewModel Constructor

public OrdersViewModel(IOrderNavigation navigationService)
{
    Orders = new ObservableCollection<Order>();
    _orderDataProvider = new OrderDataProvider();
    _navigationService = navigationService;


    GoToOrdersCommand = new RelayCommand<Order>((order) => OrderDetails((Order)order));
    GoToNewOrderCommand = new DelegateCommand(NewOrder); // Additional navigate without object

    Load();
}

Additional Command method to navigate to a New/Empty Order Detail for reference added to the OrdersViewModel

private void NewOrder()
{
    _navigationService.NavigateToOrderAsync();
}

Updated Order Navigation

public class OrderNavigationService : IOrderNavigation
{
    public void NavigateToOrdersAsync()
    {
        Shell.Current.GoToAsync(nameof(OrdersPage));
    }

    public void NavigateToOrderAsync(Order order)
    {
        Shell.Current.GoToAsync(nameof(OrderPage), true, new Dictionary<string, object> { { "Order", order } });
    }

    public void NavigateToOrderAsync()
    {
        Shell.Current.GoToAsync(nameof(OrderPage));
    }
}

OrderPage View Code Behind File

Added QueryProperty, a field and method to create an OrderViewModel if an Order object is provided.

[QueryProperty(nameof(order), "Order")]
public partial class OrderPage : ContentPage
{
    public Order order
    {
        set
        {
            Load(value);
        }
    }

    public OrderPage()
    {
        InitializeComponent();

        BindingContext = new OrderViewModel();
    }

    private void Load(Order order)
    {
        if (order != null)
            BindingContext = new OrderViewModel(order);
    }
}
Related