In the third part of the series about Dynamic Data, we covered how to group items. In this part, we will explora how to do multi-selection per row, reusing the concepts of grouping that we learned in the previous article.
- Getting Started
- Filtering and sorting
- Grouping
- Row Multi-Selection
Multi-Selection per row
As an example, we are going to create the following notifications settings page:

It has two groups Development and Administration. Inside each group, we will have these channels: Sms, Email, Push, Phone.
Let’s start!
1.Create your Model
using ReactiveUI; | |
namespace DynamicDataGroupingSample | |
{ | |
public class NotificationConfiguration : ReactiveObjectEx | |
{ | |
public NotificationConfiguration(string name, int order, string groupName, int groupOrder,bool smsChannel, bool emailChannel, bool pushNotificationChannel, bool phoneChannel) | |
{ | |
Name = name; | |
Order = order; | |
GroupName = groupName; | |
GroupOrder = groupOrder; | |
SmsChannel = smsChannel; | |
EmailChannel = emailChannel; | |
PushNotificationChannel = pushNotificationChannel; | |
PhoneChannel = phoneChannel; | |
} | |
public string Name { get;} | |
public int Order { get; } | |
//Group | |
public string GroupName { get; } | |
public int GroupOrder { get; } | |
public string GroupCssIcon { get; } | |
public string GroupFontIcon { get; } | |
//Channels | |
public bool SmsChannel | |
{ | |
get => _smsChannel; | |
set => this.RaiseAndSetIfChanged(ref _smsChannel, value); | |
} | |
public bool EmailChannel | |
{ | |
get => _emailChannel; | |
set => this.RaiseAndSetIfChanged(ref _emailChannel, value); | |
} | |
public bool PushNotificationChannel | |
{ | |
get => _pushNotificationChannel; | |
set => this.RaiseAndSetIfChanged(ref _pushNotificationChannel, value); | |
} | |
public bool PhoneChannel | |
{ | |
get => _phoneChannel; | |
set => this.RaiseAndSetIfChanged(ref _phoneChannel, value); | |
} | |
private bool _smsChannel; | |
private bool _emailChannel; | |
private bool _pushNotificationChannel; | |
private bool _phoneChannel; | |
} | |
} |
Make sure to implement PropertyChanged in the selectable properties: SmsChannel, PhoneChannel, EmailChannel and PushNotificationChannel. Since we want to detect when the checkbox has changed its state in any of them.
Note: Copy the ReactiveObjExt class here.
2.Create the Grouping List
In the ViewModel, let’s create a SourceCache and add a few items.
namespace DynamicDataGroupingSample | |
{ | |
public class NotificationConfigurationViewModel : ReactiveObjectEx | |
{ | |
public NotificationConfigurationViewModel() | |
{ | |
_notificationConfigurationsSourceCache.AddOrUpdate(new List<NotificationConfiguration>() | |
{ | |
new NotificationConfiguration("Information", 0,"Development", 0,false, false, true, false), | |
new NotificationConfiguration("Status Changed", 1,"Administration", 1, false, true, false, false), | |
new NotificationConfiguration("Error", 3,"Development", 0, false, true, false, false), | |
new NotificationConfiguration("New Report", 4,"Administration", 1, false, false, false, false), | |
new NotificationConfiguration("Warning", 1,"Development", 0, false, true, false, false), | |
new NotificationConfiguration("Security Update", 2, "Administration", 1, false, false, true, true), | |
new NotificationConfiguration("Verbose", 2, "Development", 0, false, false, false, false), | |
new NotificationConfiguration("Policy Update", 3, "Administration", 1, false, false, false, false) | |
}); | |
var notificationConfigurationItemChanges = _notificationConfigurationsSourceCache.Connect() | |
.RefCount(); | |
notificationConfigurationItemChanges | |
.Sort(SortExpressionComparer<NotificationConfiguration>.Ascending(a => a.GroupOrder)) | |
.Group(x => x.GroupName) | |
.Transform(g => new ObservableGroupedCollection<string, NotificationConfiguration, string>(g, null, Observable.Return<IComparer<NotificationConfiguration>>(SortExpressionComparer<NotificationConfiguration>.Ascending(a => a.Order)))) | |
.Bind(out _groups) | |
.DisposeMany() | |
.Subscribe() | |
.DisposeWith(Subscriptions); | |
} | |
public ReadOnlyObservableCollection<ObservableGroupedCollection<string, NotificationConfiguration, string>> NotificationGroups => _groups; | |
private readonly ReadOnlyObservableCollection<ObservableGroupedCollection<string, NotificationConfiguration, string>> _groups; | |
private SourceCache<NotificationConfiguration, string> _notificationConfigurationsSourceCache = new SourceCache<NotificationConfiguration, string>(x => x.Name); | |
} | |
} |
Note: Copy here ObservableGroupedCollection class, here.
3.Observe channel state changes
To observe when a channel has changed its state, we are going to create a reactive pipeline for each channel using WhenPropertyChanged, also using Throttle we will limit notifications emissions when selecting. Finally, will use InvokeCommand to subscribe and invoke a command when the state changes.
namespace DynamicDataGroupingSample | |
{ | |
public class NotificationConfigurationViewModel : ReactiveObjectEx | |
{ | |
public NotificationConfigurationViewModel() | |
{ | |
... | |
var smsChannelChangedCommand = ReactiveCommand.CreateFromTask<NotificationConfiguration>(OnSmsChannelChanged); | |
var emailChannelChangedCommand = ReactiveCommand.CreateFromTask<NotificationConfiguration>(OnEmailChannelChanged); | |
var pushNotificationChannelChangedCommand = ReactiveCommand.CreateFromTask<NotificationConfiguration>(OnPushNotificationChannelChanged); | |
var phoneChannelChangedCommand = ReactiveCommand.CreateFromTask<NotificationConfiguration>(OnPhoneChannelChanged); | |
notificationConfigurationItemChanges | |
.WhenPropertyChanged(x => x.SmsChannel , notifyOnInitialValue: false) | |
.Throttle(TimeSpan.FromMilliseconds(SelectionDueMilliseconds), RxApp.TaskpoolScheduler) | |
.Select(x => x.Sender) | |
.InvokeCommand(smsChannelChangedCommand) | |
.DisposeWith(Subscriptions); | |
notificationConfigurationItemChanges | |
.WhenPropertyChanged(x => x.EmailChannel, notifyOnInitialValue: false) | |
.Throttle(TimeSpan.FromMilliseconds(SelectionDueMilliseconds), RxApp.TaskpoolScheduler) | |
.Select(x => x.Sender) | |
.InvokeCommand(emailChannelChangedCommand) | |
.DisposeWith(Subscriptions); | |
notificationConfigurationItemChanges | |
.WhenPropertyChanged(x => x.PushNotificationChannel, notifyOnInitialValue: false) | |
.Throttle(TimeSpan.FromMilliseconds(SelectionDueMilliseconds), RxApp.TaskpoolScheduler) | |
.Select(x => x.Sender) | |
.InvokeCommand(pushNotificationChannelChangedCommand) | |
.DisposeWith(Subscriptions); | |
notificationConfigurationItemChanges | |
.WhenPropertyChanged(x => x.PhoneChannel, notifyOnInitialValue: false) | |
.Throttle(TimeSpan.FromMilliseconds(SelectionDueMilliseconds), RxApp.TaskpoolScheduler) | |
.Select(x => x.Sender) | |
.InvokeCommand(phoneChannelChangedCommand) | |
.DisposeWith(Subscriptions); | |
} | |
private Task OnPhoneChannelChanged(NotificationConfiguration notificationConfiguration) | |
{ | |
System.Diagnostics.Debug.WriteLine($"Phone Channel - Changed - {notificationConfiguration.GroupName}.{notificationConfiguration.Name} - {notificationConfiguration.PhoneChannel}"); | |
return Task.CompletedTask; | |
} | |
private Task OnPushNotificationChannelChanged(NotificationConfiguration notificationConfiguration) | |
{ | |
System.Diagnostics.Debug.WriteLine($"Push Notification Channel - Changed - {notificationConfiguration.GroupName}.{notificationConfiguration.Name} - {notificationConfiguration.PushNotificationChannel}"); | |
return Task.CompletedTask; | |
} | |
private Task OnEmailChannelChanged(NotificationConfiguration notificationConfiguration) | |
{ | |
System.Diagnostics.Debug.WriteLine($"Email Notification Channel - Changed - {notificationConfiguration.GroupName}.{notificationConfiguration.Name} - {notificationConfiguration.EmailChannel}"); | |
return Task.CompletedTask; | |
} | |
private Task OnSmsChannelChanged(NotificationConfiguration notificationConfiguration) | |
{ | |
System.Diagnostics.Debug.WriteLine($"SMS Notification Channel - Changed - {notificationConfiguration.GroupName}.{notificationConfiguration.Name} - {notificationConfiguration.SmsChannel}"); | |
return Task.CompletedTask; | |
} | |
private const int SelectionDueMilliseconds = 250; | |
} | |
} |
4.Create the UI
<?xml version="1.0" encoding="UTF-8"?> | |
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" | |
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" | |
x:Class="DynamicDataGroupingSample.NotificationConfigurationsPage" | |
Title="Notification Settings"> | |
<CollectionView ItemsSource="{Binding NotificationGroups}" | |
IsGrouped="True"> | |
<CollectionView.GroupHeaderTemplate> | |
<DataTemplate> | |
<Label Padding="10" | |
Text="{Binding Key}" | |
BackgroundColor="CornflowerBlue" | |
FontSize="Large" /> | |
</DataTemplate> | |
</CollectionView.GroupHeaderTemplate> | |
<CollectionView.ItemTemplate> | |
<DataTemplate> | |
<StackLayout> | |
<Label Text="{Binding Name}" FontSize="Large" TextDecorations="Underline" /> | |
<Grid RowDefinitions="Auto, Auto" ColumnDefinitions="*, *, *, *"> | |
<Label Grid.Column="0" Grid.Row="0" Text="Sms" /> | |
<CheckBox Grid.Column="0" Grid.Row="1" IsChecked="{Binding SmsChannel}"/> | |
<Label Grid.Column="1" Grid.Row="0" Text="Email" /> | |
<CheckBox Grid.Column="1" Grid.Row="1" IsChecked="{Binding EmailChannel}"/> | |
<Label Grid.Column="2" Grid.Row="0" Text="Push" /> | |
<CheckBox Grid.Column="2" Grid.Row="1" IsChecked="{Binding PushNotificationChannel}"/> | |
<Label Grid.Column="3" Grid.Row="0" Text="Phone" /> | |
<CheckBox Grid.Column="3" Grid.Row="1" IsChecked="{Binding PhoneChannel}"/> | |
</Grid> | |
</StackLayout> | |
</DataTemplate> | |
</CollectionView.ItemTemplate> | |
</CollectionView> | |
</ContentPage> |
If you haven’t fallen in love with Dynamic Data yet, stay tuned for the next part of this series with some more Dynamic Data magic tricks.
Check the full source code here.
Happy Dynamic Data!
One Comment
quite interesting to me
Comments are closed.