Created
May 12, 2021 23:45
-
-
Save JordanMarr/dfe34745a9617234a9dfb2294c382508 to your computer and use it in GitHub Desktop.
Xamarin TODO C# MVU
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}"); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
Author
JordanMarr
commented
May 12, 2021
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
{
}
}
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