Skip to content

Instantly share code, notes, and snippets.

@JordanMarr
Created May 12, 2021 23:45
Show Gist options
  • Save JordanMarr/dfe34745a9617234a9dfb2294c382508 to your computer and use it in GitHub Desktop.
Save JordanMarr/dfe34745a9617234a9dfb2294c382508 to your computer and use it in GitHub Desktop.
Xamarin TODO C# MVU
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
Shell.NavBarIsVisible="False"
Shell.TabBarIsVisible="False"
x:Name="xTodoPage"
x:Class="XamarinStore.Views.TodoPage">
<ContentPage.Resources>
<ResourceDictionary>
<xct:InvertedBoolConverter x:Key="InvertedBoolConverter" />
</ResourceDictionary>
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout Margin="5">
<Label Text="TODO" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" FontAttributes="Bold" FontSize="42" />
<CollectionView ItemsSource="{Binding Todos}">
<CollectionView.ItemTemplate>
<DataTemplate>
<Grid>
<StackLayout Orientation="Horizontal" Spacing="10" Margin="0,5,0,0">
<Button Text="Toggle" Command="{Binding BindingContext.ToggleDoneCmd, Source={x:Reference xTodoPage}}" CommandParameter="{Binding}" />
<Label Text="{Binding Description}" TextDecorations="Strikethrough" IsVisible="{Binding IsDone}" FontSize="28" />
<Label Text="{Binding Description}" TextDecorations="None" IsVisible="{Binding IsDone, Converter={StaticResource InvertedBoolConverter}}" FontSize="28" />
</StackLayout>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<StackLayout>
<Label Text="New Todo:" VerticalTextAlignment="Center" />
<Entry Text="{Binding NewDescription, Mode=TwoWay}" />
</StackLayout>
<Button Text="Add Todo" Command="{Binding AddTodoCmd}" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
namespace XamarinStore.Models
{
public record Todo(
bool IsDone,
string Description
);
public record TodoStoreModel(
string NewDescription,
Todo[] Todos
) {
public static TodoStoreModel Init => new TodoStoreModel("", Array.Empty<Todo>());
}
public class TodoStore : Store<TodoStoreModel>
{
public TodoStore() : base(TodoStoreModel.Init) { }
public record SetNewDescription(string Text) : Msg;
public record AddTodo() : Msg;
public record ToggleDone(Todo Todo) : Msg;
public override TodoStoreModel Update(TodoStoreModel model, Msg message)
{
switch (message)
{
case SetNewDescription msg:
return model with { NewDescription = msg.Text };
case AddTodo _:
return model with {
Todos = model.Todos.Union(new[] { new Todo(false, model.NewDescription) }).ToArray(),
NewDescription = ""
};
case ToggleDone msg:
return model with
{ Todos =
model.Todos
.Select(t => t.Description == msg.Todo.Description ? t with { IsDone = !t.IsDone } : t)
.ToArray()
};
default:
throw new Exception($"Unhandled Update Msg: {message.GetType().Name}");
}
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Input;
using XamarinStore.Models;
namespace XamarinStore.ViewModels
{
public class TodoViewModel : BaseViewModel
{
TodoStore _store;
public TodoViewModel(TodoStore store)
{
_store = store;
_store.Notify(m => m.NewDescription, OnPropertyChanged, () => NewDescription);
_store.Notify(m => m.Todos, OnPropertyChanged, () => Todos);
}
public string NewDescription
{
get => _store.Model.NewDescription;
set => _store.Dispatch(new TodoStore.SetNewDescription(value));
}
public Todo[] Todos => _store.Model.Todos;
public ICommand AddTodoCmd => _store.DispatchCommand(new TodoStore.AddTodo());
public ICommand ToggleDoneCmd => _store.DispatchCommand<Todo>(todo => new TodoStore.ToggleDone(todo));
}
}
@shirshov
Copy link

Here's a version using Laconic.

Full MVU, 100% C#. Just two records and two pure functions. No boilerplate, no rote.

To try it yourself do:

dotnet new --install Laconic.AppTemplate

dotnet new laconicapp -o Laconic.Todo

Open it in your IDE, replace the content of App.cs with this:

using System.Linq;
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.iOSSpecific;

namespace Laconic.Todo
{
    public class App : Xamarin.Forms.Application
    {
        public record Todo(bool IsDone, string Description);

        public record Model(string NewDescription, Todo[] Todos);

        static Model Update(Model model, Signal signal) => signal switch {
            ("new", string text) => model with { NewDescription = text },
            ("add", _) => model with { 
                Todos = model.Todos.Append(new Todo(false, model.NewDescription)).ToArray(),
                NewDescription = "",
            },
            ("toggle", Todo todo) =>  model with { 
                Todos = model.Todos
                    .Select(t => t == todo ? t with { IsDone = !t.IsDone } : t) 
                    .ToArray() }
        };

        static StackLayout View(Model model) => new() {
            ["title"] = new Label {
                Text = "TODO",
                HorizontalOptions = LayoutOptions.CenterAndExpand,
                FontAttributes = FontAttributes.Bold,
                FontSize = 42,
            },
            ["list"] = new CollectionView {
                Items = model.Todos
                    .Select((todo, index) => (todo, index))
                    .ToItemsList( _ => "todo-row", x => x.index, x => new StackLayout {
                        Orientation = StackOrientation.Horizontal,
                        Spacing = 10,
                        Margin = (0, 5, 0, 0),
                        ["btn"] = new Button {
                            Text = "Toggle",
                            Clicked = () => new("toggle", x.todo),
                        },
                        ["lbl"] = new Label {
                            TextType = TextType.Html,
                            Text = x.todo.IsDone ? $"<s>{x.todo.Description}</s>" : x.todo.Description,
                            FontSize = 28,
                        }
                    })
            },
            ["entry"] = new StackLayout {
                ["lbl"] = new Label {
                    Text = "New Todo:"
                },
                ["txt"] = new Entry {
                    Text = model.NewDescription,
                    TextChanged = e => new("new", e.NewTextValue)
                }
            },
            ["add-btn"] = new Button {
                Text = "Add Todo",
                Clicked = () => new("add"),
                IsEnabled = model.NewDescription != ""
            }
        };

        readonly Binder<Model> _binder;
        
        public App()
        {
            _binder = Binder.Create(new Model("", new Todo[0]), Update);
            var p = _binder.CreateElement(s => new ContentPage { Content = View(s) });
            p.On<iOS>().SetUseSafeArea(true);
            MainPage = p;
        }
    }
}

// Temporary stub: records won't work without this 
namespace System.Runtime.CompilerServices
{
    sealed class IsExternalInit
    {
    }
}

@JordanMarr
Copy link
Author

Very nice -- I love it!
I already did install the Laconic template when I was experimenting with it the other week.
The only reason I'm not using it is because of hot reload. Don Syme create a hot reload mechanism for Fabulous that recompiles your changes to the code on-the-fly; without a hot reload mechanism, and no preview, it would be laborious.

Sadly, even the xaml hot reload breaks pretty easily which forces many reloads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment