22

TODO List using Reactive UI in Xamarin Forms

 3 years ago
source link: https://www.xamboy.com/2021/06/22/todo-list-using-reactive-ui-in-xamarin-forms/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

During the past months, I have been learning and writing about Dynamic Data and how we can use it to handle our collections in a reactive way. In this article, I will show you a full sample of how to use the Reactive UI framework in Xamarin Forms by creating a ToDo List.

ReactiveUI is a Framework that gives you the power to build reactive, testable, and composable UI code using the MVVM pattern.”.

Let’s start!

1. Install the ReactiveUI.XamForms NuGet package

2. Install the DynamicData NuGet package

We are going to use it to handle our ToDo list.

3. Install the Sextant.XamForms package

We are going to use it to handle the page navigation.

4. Create the structure of the TODO

  • Create a HomePage -> HomePageViewModel, to handle the list of TODOs.
  • Create an ItemPage -> ItemViewModel, to handle adding and editing items in the TODO list.
  • Create a Model -> Item. (Represents our ToDo item)
  • Create an ItemManager to handle our ToDo collection.

5. Add the following classes

ViewModelBase -> Base class for all ViewModels.

namespace ReactiveToDoSample.ViewModels { public abstract class ViewModelBase : ReactiveObject, IDisposable, INavigable { protected ViewModelBase(IParameterViewStackService viewStackService) => NavigationService = viewStackService;

public abstract string Id { get; }

public virtual IObservable<Unit> WhenNavigatedFrom(INavigationParameter parameter) => Observable.Return(Unit.Default);

public virtual IObservable<Unit> WhenNavigatedTo(INavigationParameter parameter) => Observable.Return(Unit.Default);

public virtual IObservable<Unit> WhenNavigatingTo(INavigationParameter parameter) => Observable.Return(Unit.Default);

protected IParameterViewStackService NavigationService { get; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }

protected virtual void Dispose(bool disposing) { if (disposing) { Subscriptions?.Dispose(); } } protected readonly CompositeDisposable Subscriptions = new CompositeDisposable(); } }

RxExceptionHandler -> Class to handle exceptions.

namespace ReactiveToDoSample { public class RxExceptionHandler : IObserver<Exception> { public void OnNext(Exception ex) { if (Debugger.IsAttached) { Debugger.Break(); }

RxApp.MainThreadScheduler.Schedule(() => { throw ex; }); }

public void OnError(Exception ex) { if (Debugger.IsAttached) { Debugger.Break(); }

RxApp.MainThreadScheduler.Schedule(() => { throw ex; }); }

public void OnCompleted() { if (Debugger.IsAttached) { Debugger.Break(); }

RxApp.MainThreadScheduler.Schedule(() => { throw new NotImplementedException(); }); } } }

These classes were taken from this Sample project.

NavigationParameterConstants -> Class for navigation constants

namespace ReactiveToDoSample { public class NavigationParameterConstants { public const string ItemId = "ItemId"; } }

6. Register Views, ViewModels and Services in the App.xaml.cs

public partial class App : Application { public App() { InitializeComponent();

RxApp.DefaultExceptionHandler = new RxExceptionHandler();

Instance.InitializeForms();

Locator .CurrentMutable .RegisterConstant<IItemManager>(new ItemManager());

Locator .CurrentMutable .RegisterNavigationView(() => new NavigationView(RxApp.MainThreadScheduler, RxApp.TaskpoolScheduler, ViewLocator.Current)) .RegisterParameterViewStackService() .RegisterView<HomePage, HomeViewModel>() .RegisterView<ItemPage, ItemViewModel>() .RegisterViewModel(() => new HomeViewModel(Locator.Current.GetService<IParameterViewStackService>(), Locator.Current.GetService<IItemManager>())) .RegisterViewModel(() => new ItemViewModel(Locator.Current.GetService<IParameterViewStackService>(), Locator.Current.GetService<IItemManager>()));

Locator .Current .GetService<IParameterViewStackService>() .PushPage<HomeViewModel>(null, true, false) .Subscribe();

MainPage = Locator.Current.GetNavigationView("NavigationView"); } }

7. Create the Item Model

It will have 3 main properties: Id, Title and a mutable IsCompleted property, which will indicate when the item has been completed.

namespace ReactiveToDoSample.Models { public class Item : ReactiveObject { public Item(string id, string title) { Id = id; Title = title; }

public string Id { get; }

public string Title { get; }

public bool IsCompleted { get => _isCompleted; set => this.RaiseAndSetIfChanged(ref _isCompleted, value); }

private bool _isCompleted; } }

8. Create the Item Manager

This manager will handle all the logic related to adding/removing/getting items.

namespace ReactiveToDoSample.Managers { public interface IItemManager { public IObservable<IChangeSet<Item, string>> ItemChanges { get; }

public Optional<Item> Get(string id);

public void AddOrUpdate(Item item);

public void Remove(Item item); } }

public class ItemManager : IItemManager { public ItemManager() { ItemChanges = _itemsCache.Connect() .RefCount(); }

public Optional<Item> Get(string id) => _itemsCache.Lookup(id);

public IObservable<IChangeSet<Item, string>> ItemChanges { get; }

public void AddOrUpdate(Item item) => _itemsCache.AddOrUpdate(item);

public void Remove(Item item) => _itemsCache.Remove(item);

private SourceCache<Item, string> _itemsCache = new SourceCache<Item, string>(item => item.Id); }

9. Create the HomePage/ViewModel

In the UI we will have a simple CollectionView and an Add button.

<?xml version="1.0" encoding="UTF-8" ?> <rxui:ReactiveContentPage x:Class="ReactiveToDoSample.Views.HomePage" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms" xmlns:vm="clr-namespace:ReactiveToDoSample.ViewModels" x:Name="homePage" Title="Reactive ToDo" x:TypeArguments="vm:HomeViewModel"> <StackLayout Padding="20"> <CollectionView ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" SelectionMode="Single"> <CollectionView.ItemsLayout> <GridItemsLayout Orientation="Vertical" VerticalItemSpacing="10" /> </CollectionView.ItemsLayout> <CollectionView.ItemTemplate> <DataTemplate> <Frame Style="{StaticResource CardFrameStyle}"> <Frame.Triggers> <DataTrigger TargetType="Frame" Binding="{Binding IsCompleted}" Value="True"> <Setter Property="Opacity" Value="0.2" /> </DataTrigger> </Frame.Triggers> <StackLayout Orientation="Horizontal"> <CheckBox IsChecked="{Binding IsCompleted}"/> <Label Text="{Binding Title}" HorizontalOptions="FillAndExpand" FontAttributes="Bold" VerticalOptions="Center"/>

<Label Text="🗑" VerticalOptions="EndAndExpand"> <Label.GestureRecognizers> <TapGestureRecognizer Command="{Binding Source={x:Reference homePage}, Path=BindingContext.DeleteCommand}" CommandParameter="{Binding}"/> </Label.GestureRecognizers> </Label> </StackLayout> </Frame> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> <Button Style="{StaticResource CircularButtonStyle}" Command="{Binding AddCommand}" Text="+"/> </StackLayout> </rxui:ReactiveContentPage>

In the HomeViewModel we will use the ItemManager to get the items, a DeleteCommand to remove elements, and an Add Command to Add/Edit an item, this command navigates to the ItemViewModel.

namespace ReactiveToDoSample.ViewModels { public class HomeViewModel : ViewModelBase { public HomeViewModel(IParameterViewStackService navigationService, IItemManager itemManager) : base(navigationService) { DeleteCommand = ReactiveCommand.Create<Item>(itemManager.Remove);

itemManager .ItemChanges .Bind(out _items) .DisposeMany() .Subscribe() .DisposeWith(Subscriptions);

itemManager.AddOrUpdate(new Item(Guid.NewGuid().ToString(), "Family vacation planning")); itemManager.AddOrUpdate(new Item(Guid.NewGuid().ToString(), "Buy Christmas Gifts")); itemManager.AddOrUpdate(new Item(Guid.NewGuid().ToString(), "Go to the Bank")); itemManager.AddOrUpdate(new Item(Guid.NewGuid().ToString(), "Buy Milk"));

AddCommand = ReactiveCommand.CreateFromObservable(() => NavigationService.PushModal<ItemViewModel>());

ViewCommand = ReactiveCommand.CreateFromObservable<Item, Unit>((item) => { SelectedItem = null; return NavigationService.PushModal<ItemViewModel>(new NavigationParameter() { { NavigationParameterConstants.ItemId , item.Id } }); });

this.WhenAnyValue(x => x.SelectedItem) .Where(x => x != null) .InvokeCommand(ViewCommand) .DisposeWith(Subscriptions);

}

public ReactiveCommand<Unit, Unit> AddCommand { get; }

public ReactiveCommand<Item, Unit> ViewCommand { get; }

public ReactiveCommand<Item, Unit> DeleteCommand { get; }

public Item SelectedItem { get => _selectedItem; set => this.RaiseAndSetIfChanged(ref _selectedItem, value); }

public ReadOnlyObservableCollection<Item> Items => _items;

public override string Id => "Reactive ToDo";

private readonly ReadOnlyObservableCollection<Item> _items; private Item _selectedItem; } }

10. Create the ItemPage/ItemModel

A simple page with an entry and an add button.

<?xml version="1.0" encoding="UTF-8" ?> <rxui:ReactiveContentPage x:Class="ReactiveToDoSample.Views.ItemPage" xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms" xmlns:vm="clr-namespace:ReactiveToDoSample.ViewModels" x:TypeArguments="vm:ItemViewModel"> <rxui:ReactiveContentPage.ToolbarItems> <ToolbarItem Command="{Binding CloseCommand}" Text="Close" /> </rxui:ReactiveContentPage.ToolbarItems> <StackLayout> <Label Text="Item" FontSize="Title" Padding="20" HorizontalOptions="Center"/> <Frame Style="{StaticResource CardFrameStyle}" Margin="10" VerticalOptions="Start"> <StackLayout HorizontalOptions="FillAndExpand"> <Entry Placeholder="Title" Text="{Binding Title}" /> <Button Command="{Binding SaveCommand}" Style="{StaticResource MainButtonStyle}" Text="Save"/> </StackLayout> </Frame> </StackLayout> </rxui:ReactiveContentPage>

This ViewModel will receive the ItemId passed from the HomeViewModel and the ItemManager will add or update that item. If no ItemId passed will create a new Item.

namespace ReactiveToDoSample.ViewModels { public class ItemViewModel : ViewModelBase { public ItemViewModel(IParameterViewStackService navigationService, IItemManager itemManager) : base(navigationService) { _itemManager = itemManager;

var canExecute = this.WhenAnyValue(x => x.Title, (title) => !string.IsNullOrEmpty(title));

SaveCommand = ReactiveCommand.Create(ExecuteSave, canExecute);

CloseCommand = ReactiveCommand.CreateFromObservable(() => NavigationService.PopModal());

SaveCommand .InvokeCommand(CloseCommand) .DisposeWith(Subscriptions);

this.WhenAnyValue(x => x.ItemId) .Where(x => x != null) .Select(x => _itemManager.Get(x)) .Where(x => x.HasValue) .Select(x => x.Value) .Subscribe(x => { Title = x.Title;

}) .DisposeWith(Subscriptions); }

public override IObservable<Unit> WhenNavigatingTo(INavigationParameter parameter) { if (parameter.TryGetValue(NavigationParameterConstants.ItemId, out string itemId)) { ItemId = itemId; }

return base.WhenNavigatedTo(parameter); }

private void ExecuteSave() => _itemManager.AddOrUpdate(new Item(ItemId ?? Guid.NewGuid().ToString(), Title));

public ReactiveCommand<Unit, Unit> SaveCommand { get; }

public ReactiveCommand<Unit, Unit> CloseCommand { get; }

public override string Id => string.Empty;

public string Title { get => _title; set => this.RaiseAndSetIfChanged(ref _title, value); }

private string ItemId { get => _itemId; set => this.RaiseAndSetIfChanged(ref _itemId, value); }

private string _title; private string _description; private readonly IItemManager _itemManager; private string _itemId; } }

Result

You can find the full source code of the sample here.

Happy Reactive ToDo!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK