Skip to main content
All docs
V24.2

DevExpress v24.2 Update — Your Feedback Matters

Our What's New in v24.2 webpage includes product-specific surveys. Your response to our survey questions will help us measure product satisfaction for features released in this major update and help us refine our plans for our next major release.

Take the survey Not interested

DevExpress MVVM Utility Classes for .NET MAUI

  • 7 minutes to read

DevExpress ships MVVM utility classes with .NET MAUI components. These classes extend component functionality and include multiple advanced features, such as enhanced data binding, event handling, and performance-optimized collections.

#Observable Object

DXObservableObject is an abstract class derived from the ObservableObject. The DXObservableObject class extends standard property change notifications and has multiple SetProperty() overloads with different callback types. A SetProperty() method triggers PropertyChanging and PropertyChanged events that allow you to execute specific actions when a property value changes. You can use DXObservableObject in the Model-View-ViewModel (MVVM) architectural design pattern.

The following example displays data stored in a DXObservableObject descendant instance and updates the UI if a user changes data:

public class OrdersViewModel : DXObservableObject
{
    private readonly IOrderService orderService;

    public OrdersViewModel(IOrderService orderService)
    {
        this.orderService = orderService;
        LoadOrderCommand = new Command(OnLoadOrder);
    }

    private int selectedOrderId;
    public int SelectedOrderId
    {
        get => selectedOrderId;
        set => SetProperty(ref selectedOrderId, value);
    }

    private Order? currentOrder;
    public Order? CurrentOrder
    {
        get => currentOrder;
        set => SetProperty(
            ref currentOrder,
            value,
            () => {
                if (value == null)
                    Console.WriteLine("No order selected.");
                else
                    Console.WriteLine($"Order #{value.Id} for {value.CustomerName} loaded.");
            }
        );
    }

    public Command LoadOrderCommand { get; }

    private async void OnLoadOrder()
    {
        CurrentOrder = await orderService.GetOrderByIdAsync(SelectedOrderId);
    }    
}

public class Order
{
    public int Id { get; set; }
    public string? CustomerName { get; set; }
    // Other order fields
}

public interface IOrderService
{
    Task<Order?> GetOrderByIdAsync(int orderId);
    // Additional methods
}

public class OrderService : IOrderService
{
    // A data retrieval placeholder method. Replace it with a real API call.
    public Task<Order?> GetOrderByIdAsync(int orderId)
    {
        return Task.FromResult(new Order
        {
            Id = orderId,
            CustomerName = $"Customer #{orderId}"
        });
    }
}

The OnLoadOrder() method retrieves an order by its identifier stored in the SelectedOrderId property and assigns the result to the CurrentOrder property. The CurrentOrder setter calls the specified callback that displays a message when a new order is loaded or no order is selected.

#Collections

DevExpress MAUI contains two classes designed to manage collections: DXObservableCollection and DXReadOnlyObservableCollection.

#DXObservableCollection

DXObservableCollection is a generic collection class that extends the standard ObservableCollection<T> class functionality. The DXObservableCollection class includes the following extra methods that allow you to perform efficient batch operations on the collection:

AddRange(IEnumerable<T>)
Adds the specified item collection to the DXObservableCollection<T>.
InsertRange(Int32, IEnumerable<T>)
Inserts the target item collection to the DXObservableCollection<T> after the specified index.
RemoveRange(Int32, Int32)
Removes the range of items from the DXObservableCollection<T>.

DXObservableCollection also includes events that allow you to track collection changes and execute custom logic:

ItemsAdding
Occurs before an item range is added.
ItemsAdded
Occurs when an item range is added.
ItemsRemoving
Occurs before an item range is removed.
ItemsRemoved
Occurs when an item range is removed.

The following example uses ItemsAdded and ItemsRemoved events to subscribe to collection events:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class ProductsViewModel : DXObservableObject
{
    public DXObservableCollection<Product> Products { get; } = new DXObservableCollection<Product>();

    public Command AddProductsCommand { get; }
    public Command RemoveLastProductCommand { get; }

    public ProductsViewModel()
    {
        AddProductsCommand = new Command(AddProducts);
        RemoveLastProductCommand = new Command(RemoveLastProduct);

        // Subscribe to collection events
        Products.ItemsAdded += (sender, e) =>
        {
            Console.WriteLine($"{e.Items.Count} items were added to the collection.");
        };
        Products.ItemsRemoved += (sender, e) =>
        {
            Console.WriteLine($"{e.Items.Count} items were removed from the collection.");
        };
    }

    private void AddProducts()
    {
        var newProducts = new List<Product>
        {
            new Product { Id = 1, Name = "Laptop", Price = 999.99m },
            new Product { Id = 2, Name = "Smartphone", Price = 699.99m },
            new Product { Id = 3, Name = "Tablet", Price = 399.99m }
        };

        Products.AddRange(newProducts);
    }

    private void RemoveLastProduct()
    {
        if (Products.Count > 0)
        {
            Products.RemoveAt(Products.Count - 1);
        }
    }
}
<dx:DXCollectionView ItemsSource="{Binding Products}" >
    <dx:DXCollectionView.ItemTemplate>
        <DataTemplate>
            <dx:DXStackLayout Padding="10" Orientation="Horizontal">
                <Label Text="{Binding Name}"/>
                <Label Text="{Binding Price, StringFormat=' - {0:C}'}" HorizontalOptions="EndAndExpand" />
            </dx:DXStackLayout>
        </DataTemplate>
    </dx:DXCollectionView.ItemTemplate>
</dx:DXCollectionView>
<dx:DXButton Content="Add Products" Command="{Binding AddProductsCommand}" />
<dx:DXButton Content="Remove Last Product" Command="{Binding RemoveLastProductCommand}" />

#DXReadOnlyObservableCollection

The DXReadOnlyObservableCollection class is a read-only wrapper around DXObservableCollection and ObservableCollection<T>. The DXReadOnlyObservableCollection class allows you to expose a collection to the UI or other components and ensures that related data is read-only. The class provides access to CollectionChanged and PropertyChanged events that allow you to track changes in the related collection.

The following example displays data stored in the DXReadOnlyObservableCollection:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class ProductsViewModel : DXObservableObject
{
    private readonly DXObservableCollection<Product> products = new DXObservableCollection<Product>();
    public DXReadOnlyObservableCollection<Product> ReadOnlyProducts { get; }
    public Command AddProductsCommand { get; }
    public ProductsViewModel()
    {
        ReadOnlyProducts = new DXReadOnlyObservableCollection<Product>(products);
        AddProductsCommand = new Command(AddProducts);
    }
    private void AddProducts()
    {
        products.Add(new Product { Id = 1, Name = "Monitor", Price = 199.99m });
        products.Add(new Product { Id = 2, Name = "Keyboard", Price = 49.99m });
        products.Add(new Product { Id = 3, Name = "Mouse", Price = 29.99m });
    }
}
<dx:DXCollectionView ItemsSource="{Binding ReadOnlyProducts}" >
    <dx:DXCollectionView.ItemTemplate>
        <DataTemplate>
            <dx:DXStackLayout Orientation="Horizontal">
                <Label Text="{Binding Name}"/>
                <Label Text="{Binding Price, StringFormat=' - {0:C}'}" HorizontalOptions="EndAndExpand" />
            </dx:DXStackLayout>
        </DataTemplate>
    </dx:DXCollectionView.ItemTemplate>
</dx:DXCollectionView>
<dx:DXButton Content="Add Products" Command="{Binding AddProductsCommand}" />

#Enumerable Extensions

The EnumerableExtensions class provides access to extension methods that work with IEnumerable<T> collections. These methods simplify data processing tasks such as filtering and collection management. You can use the following extension methods:

Yield<T>(T)
Converts an individual item into an IEnumerable<T> collection that contains this item. If the specified item is null, the method returns the collection that stores null.
YieldIfNotNull<T>(T)
Converts an individual item into an IEnumerable<T> sequence that contains this item. If the specified item is null, the method returns an empty sequence.
IsSingle<T>(IEnumerable<T>)
Determines whether the specified IEnumerable<T> collection contains only one element.
ForEach(Action<T, Int32>, Boolean)
Iterates through each item in the DXObservableCollection<T> and performs the specified action.
Flatten<T>(IEnumerable<T>, Func<T, IEnumerable<T>>)
Converts a hierarchical data structure into a flat collection. This method traverses the hierarchy defined by the getItems() function. The function accepts all hierarchy items and returns nested items as a flat collection.
ToReadOnlyCollection() | ToObservableCollection() | ToReadOnlyObservableCollection()
Converts an IEnumerable<T> collection to specific collection types (read-only or observable).

The following example uses the Flatten() method to convert a hierarchical data structure into a flat collection. The ForEach(name => FlattenedItems.Add(name)) method iterates through the flattened node name collection and adds each name to the FlattenedItems collection:

public class TreeNode
{
    public string Name { get; set; }
    public List<TreeNode> Children { get; set; } = new();
}

public class TreeViewModel : DXObservableObject
{
    private readonly DXObservableCollection<TreeNode> treeNodes = new();
    public DXObservableCollection<string> FlattenedItems { get; } = new();

    public Command LoadItemsCommand { get; }

    public TreeViewModel()
    {
        LoadItemsCommand = new Command(LoadItems);
    }

    private void LoadItems()
    {
        // Clear previous data
        treeNodes.Clear();
        FlattenedItems.Clear();

        // Create hierarchical data
        var root1 = new TreeNode
        {
            Name = "Root 1",
            Children = new List<TreeNode>
            {
                new TreeNode { Name = "Child 1.1" },
                new TreeNode { Name = "Child 1.2" }
            }
        };
        var root2 = new TreeNode
        {
            Name = "Root 2",
            Children = new List<TreeNode>
            {
                new TreeNode { Name = "Child 2.1" },
                new TreeNode
                {
                    Name = "Child 2.2",
                    Children = new List<TreeNode>
                    {
                        new TreeNode { Name = "Child 2.2.1" }
                    }
                }
            }
        };

        treeNodes.Add(root1);
        treeNodes.Add(root2);

        // Flatten hierarchical data and add to FlattenedItems
        treeNodes
            .Flatten(node => node.Children)
            .Select(node => node.Name)
            .ForEach(name => FlattenedItems.Add(name));
    }
}
<dx:DXCollectionView ItemsSource="{Binding FlattenedItems}">
    <dx:DXCollectionView.ItemTemplate>
        <DataTemplate>
            <Label Text="{Binding}" />
        </DataTemplate>
    </dx:DXCollectionView.ItemTemplate>
</dx:DXCollectionView>
<dx:DXButton Content="Load Hierarchical Items" Command="{Binding LoadItemsCommand}" />

#Weak Event

The WeakEvent<TEventHandler, TEventArgs> class includes a mechanism that allows you to manage event handlers without the need to create strong references between the event publisher and subscribers. Weak events prevent memory leaks that may occur when objects remain referenced due to event subscriptions.

The following example uses the WeakEvent<TEventHandler, TEventArgs> class to notify subscribers when an event occurs:

public class NotificationPublisher
{
    private readonly WeakEvent<EventHandler<string>, string> notificationEvent = new();

    public ICommand SendNotificationCommand { get; }

    public NotificationPublisher()
    {
        SendNotificationCommand = new Command(() => Notify("Notification sent through WeakEvent!"));
    }

    public void Subscribe(EventHandler<string> handler)
    {
        notificationEvent.Add(handler);
    }

    public void Notify(string message)
    {
        notificationEvent.Raise(this, message);
    }
}
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();

        var publisher = (NotificationPublisher)BindingContext;

        // Subscriber 1
        publisher.Subscribe((sender, message) =>
        {
            Console.WriteLine($"Subscriber 1 received: {message}");
        });

        // Subscriber 2
        publisher.Subscribe((sender, message) =>
        {
            Console.WriteLine($"Subscriber 2 received: {message}");
        });
    }
}