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 isnull
, the method returns the collection that storesnull
. - YieldIfNotNull<T>(T)
- Converts an individual item into an
IEnumerable<T>
sequence that contains this item. If the specified item isnull
, 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}");
});
}
}