Created
August 7, 2020 02:11
-
-
Save programmation/7686241052b38de37222a6d8a7a7d476 to your computer and use it in GitHub Desktop.
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; | |
using System.Collections.Generic; | |
using System.Threading.Tasks; | |
using System.Threading.Tasks.Dataflow; | |
using ICountingTransform = Messaging.ITransform<Messaging.CountingMessage, Messaging.CountingState>; | |
using ICountingSideEffect = Messaging.ISideEffect<Messaging.CountingView, Messaging.CountingMessage, Messaging.CountingState>; | |
namespace Messaging | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
Console.WriteLine("Hello World!"); | |
} | |
} | |
public class StateHistory<TMessage, TState> | |
where TMessage : Message | |
where TState : State | |
{ | |
public DateTime When { get; } | |
public TState State { get; } | |
public TMessage? Message { get; } | |
public StateHistory(TState state, TMessage? message = null) | |
{ | |
State = state; | |
Message = message; | |
When = DateTime.Now; | |
} | |
} | |
// Abstract stateful view types | |
public abstract record State { } | |
public abstract record Message { } | |
public abstract class View { } | |
public interface ITransform<TMessage, TState> | |
where TMessage : Message | |
where TState : State | |
{ | |
IList<StateHistory<TMessage, TState>> StateHistory { get; } | |
void Initialize(TState initialState); | |
TState Transform(TMessage message, TState state); | |
} | |
public interface ISideEffect<TStatefulView, TMessage, TState> | |
where TStatefulView : StatefulView<TStatefulView, TMessage, TState> | |
where TMessage : Message | |
where TState : State | |
{ | |
void AddSideEffect<T>(Action<TStatefulView, TMessage, TState, TState> action) where T : TMessage; | |
void RemoveSideEffect<T>() where T : TMessage; | |
void Run(TStatefulView statefulView, TMessage message, TState oldState, TState newState); | |
} | |
public abstract class StatefulView<TStatefulView, TMessage, TState> | |
where TStatefulView : StatefulView<TStatefulView, TMessage, TState> | |
where TMessage : Message | |
where TState : State | |
{ | |
public ActionBlock<TMessage> Queue { get; private set; } | |
public TState State { get; private set; } = null!; | |
public ITransform<TMessage, TState> Transform { get; private set; } = null!; | |
public ISideEffect<TStatefulView, TMessage, TState>? SideEffect { get; private set; } = null!; | |
private StatefulView() | |
{ | |
Queue = new ActionBlock<TMessage>(ProcessMessage); | |
State = InitialState; | |
} | |
public StatefulView(ITransform<TMessage, TState> transform) | |
: this() | |
{ | |
Transform = transform; | |
Transform.Initialize(State); | |
} | |
public StatefulView(ITransform<TMessage, TState> transform, ISideEffect<TStatefulView, TMessage, TState> sideEffect) | |
: this(transform) | |
{ | |
SideEffect = sideEffect; | |
} | |
public void Dispatch(TMessage message) | |
{ | |
Queue.Post(message); | |
} | |
public void ProcessMessage(TMessage message) | |
{ | |
var oldState = State; | |
var newState = Transform.Transform(message, oldState); | |
SideEffect?.Run((TStatefulView)this, message, oldState, newState); | |
State = newState; | |
} | |
public abstract TState InitialState { get; } | |
public abstract View View(TState state); | |
} | |
public abstract class StateTransform<TMessage, TState> : ITransform<TMessage, TState> | |
where TMessage : Message | |
where TState : State | |
{ | |
public IList<StateHistory<TMessage, TState>> StateHistory { get; private set; } | |
= new List<StateHistory<TMessage, TState>>(); | |
public void Initialize(TState initialState) | |
{ | |
StateHistory.Add(new StateHistory<TMessage, TState>(initialState)); | |
} | |
public virtual TState Transform(TMessage message, TState state) | |
{ | |
return Remember(message, state); | |
} | |
protected virtual TState Remember(TMessage message, TState state) | |
{ | |
StateHistory.Add(new StateHistory<TMessage, TState>(state, message)); | |
return state; | |
} | |
} | |
public abstract class SideEffect<TStatefulView, TMessage, TState> | |
: ISideEffect<TStatefulView, TMessage, TState> | |
where TStatefulView : StatefulView<TStatefulView, TMessage, TState> | |
where TMessage : Message | |
where TState : State | |
{ | |
private readonly IDictionary<Type, Action<TStatefulView, TMessage, TState, TState>> Actions | |
= new Dictionary<Type, Action<TStatefulView, TMessage, TState, TState>>(); | |
public void AddSideEffect<T>(Action<TStatefulView, TMessage, TState, TState> action) | |
where T : TMessage | |
{ | |
Actions[typeof(T)] = action; | |
} | |
public void RemoveSideEffect<T>() | |
where T : TMessage | |
{ | |
Actions.Remove(typeof(T)); | |
} | |
public void Run(TStatefulView statefulView, TMessage message, TState oldState, TState newState) | |
{ | |
var type = message.GetType(); | |
if (!Actions.ContainsKey(type)) | |
return; | |
var action = Actions[type]; | |
action.Invoke(statefulView, message, oldState, newState); | |
} | |
} | |
// Application state | |
public record CountingState : State | |
{ | |
public static CountingState Initial() | |
=> new CountingState(1); | |
public CountingState(int step) | |
{ | |
Step = step; | |
} | |
public int Count { get; set; } | |
public int Step { get; set; } | |
public bool IsTimerEnabled { get; set; } | |
} | |
// Application messages | |
public abstract record CountingMessage : Message { } | |
public record IncrementMessage : CountingMessage { } | |
public record DecrementMessage : CountingMessage { } | |
public record ResetMessage : CountingMessage { } | |
public record SetStepMessage : CountingMessage | |
{ | |
public int Step { get; } | |
public SetStepMessage(int step) | |
{ | |
Step = step; | |
} | |
} | |
public record TimerOnMessage : CountingMessage | |
{ | |
public int Ticks { get; } | |
public TimerOnMessage(int ticks) | |
{ | |
Ticks = ticks; | |
} | |
} | |
public record TimerOffMessage : CountingMessage { } | |
public record TimerTickMessage : CountingMessage { } | |
// Convenient properties and constructors | |
// -- Messages with no parameters can be static properties | |
// -- Messages with parameters need to be a new object for each invocation | |
public static class Messages | |
{ | |
public static IncrementMessage IncrementMessage { get; } = new IncrementMessage(); | |
public static DecrementMessage DecrementMessage { get; } = new DecrementMessage(); | |
public static SetStepMessage SetStepMessage(int step) => new SetStepMessage(step); | |
public static TimerOnMessage TimerOnMessage(int ticks) => new TimerOnMessage(ticks); | |
public static TimerOffMessage TimerOffMessage { get; } = new TimerOffMessage(); | |
public static TimerTickMessage TimerTickMessage { get; } = new TimerTickMessage(); | |
public static ResetMessage ResetMessage { get; } = new ResetMessage(); | |
} | |
// Message transformer | |
public class CountingTransform : StateTransform<CountingMessage, CountingState> | |
{ | |
public override CountingState Transform(CountingMessage message, CountingState state) | |
{ | |
var newState = message switch | |
{ | |
IncrementMessage => state with { Count = state.Count + state.Step }, | |
DecrementMessage => state with { Count = state.Count - state.Step }, | |
SetStepMessage m => state with { Step = m.Step }, | |
TimerOnMessage => state with { IsTimerEnabled = true }, | |
TimerOffMessage => state with { IsTimerEnabled = false }, | |
TimerTickMessage => state.IsTimerEnabled | |
? Transform(Messages.IncrementMessage, state) | |
: state, | |
ResetMessage => StateHistory[0].State, | |
_ => state | |
}; | |
return base.Transform(message, newState); | |
} | |
} | |
// Stateful view implementation | |
public class CountingView : | |
StatefulView<CountingView, CountingMessage, CountingState> | |
{ | |
public override CountingState InitialState => CountingState.Initial(); | |
public CountingView(ICountingTransform transform) | |
: base(transform) | |
{ | |
} | |
public CountingView(ICountingTransform transform, ICountingSideEffect sideEffect) | |
: base(transform, sideEffect) | |
{ | |
} | |
// View and active objects | |
public override View View(CountingState state) | |
=> new VStack | |
{ | |
new Text($"{state.Count}"), | |
new Button("Increment", () => Dispatch(Messages.IncrementMessage)), | |
new Button("Decrement", () => Dispatch(Messages.DecrementMessage)), | |
new HStack | |
{ | |
new Text("Timer"), | |
new Toggle(state.IsTimerEnabled, | |
(bool isEnabled) | |
=> Dispatch(isEnabled | |
? (CountingMessage)Messages.TimerOnMessage(100) | |
: Messages.TimerOffMessage)) | |
}, | |
new Slider(state.Step, 10, (int step) => Dispatch(Messages.SetStepMessage(step))), | |
new Text($"Step Size: {state.Step}"), | |
new Button("Reset", () => Dispatch(Messages.ResetMessage)) | |
}; | |
} | |
public class CountingSideEffect : SideEffect<CountingView, CountingMessage, CountingState> | |
{ | |
private CountingTimer _timer = null!; | |
public CountingSideEffect() | |
{ | |
AddSideEffect<TimerOnMessage>(TimerOnSideEffect); | |
AddSideEffect<TimerOffMessage>(TimerOffSideEffect); | |
AddSideEffect<ResetMessage>(HandleReset); | |
} | |
private void TimerOnSideEffect(CountingView view, CountingMessage message, CountingState oldState, CountingState newState) | |
{ | |
if (!(message is TimerOnMessage timerOnMessage)) | |
return; | |
if (oldState.IsTimerEnabled) | |
return; | |
if (newState.IsTimerEnabled) | |
{ | |
_timer = new CountingTimer(timerOnMessage.Ticks, () => view.Dispatch(Messages.TimerTickMessage)); | |
_timer.Start(); | |
} | |
} | |
private void TimerOffSideEffect(CountingView view, CountingMessage message, CountingState oldState, CountingState newState) | |
{ | |
if (!(message is TimerOffMessage)) | |
return; | |
if (!oldState.IsTimerEnabled) | |
return; | |
if (!newState.IsTimerEnabled) | |
{ | |
_timer.Stop(); | |
_timer = null!; | |
} | |
} | |
private void HandleReset(CountingView view, CountingMessage message, CountingState oldState, CountingState newState) | |
{ | |
if (oldState.IsTimerEnabled) | |
{ | |
_timer.Stop(); | |
_timer = null!; | |
} | |
} | |
} | |
public class CountingTimer | |
{ | |
private ActionBlock<int> _queue; | |
private bool _isRunning; | |
public int Delay { get; } | |
public Action Callback { get; } | |
public CountingTimer(int delay, Action callback) | |
{ | |
Delay = delay; | |
Callback = callback; | |
_queue = new ActionBlock<int>(HandleAction); | |
} | |
public void Start() | |
{ | |
if (_isRunning) | |
return; | |
_isRunning = true; | |
_queue.Post(Delay); | |
} | |
private void HandleAction(int delay) | |
{ | |
if (!_isRunning) | |
return; | |
Task.Delay(delay) | |
.ContinueWith(t => | |
{ | |
if (!_isRunning) | |
return; | |
Callback?.Invoke(); | |
_queue.Post(delay); | |
}); | |
} | |
public void Stop() | |
{ | |
if (!_isRunning) | |
return; | |
_isRunning = false; | |
} | |
} | |
// Demo View classes | |
public class HStack : View, IEnumerable<View> | |
{ | |
public IEnumerator<View> GetEnumerator() | |
{ | |
throw new NotImplementedException(); | |
} | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
throw new NotImplementedException(); | |
} | |
public void Add(View view) { } | |
} | |
public class VStack : View, IEnumerable<View> | |
{ | |
public IEnumerator<View> GetEnumerator() | |
{ | |
throw new NotImplementedException(); | |
} | |
IEnumerator IEnumerable.GetEnumerator() | |
{ | |
throw new NotImplementedException(); | |
} | |
public void Add(View view) { } | |
} | |
public class Text : View | |
{ | |
public Text(string text) { } | |
} | |
public class Toggle : View | |
{ | |
public Toggle(bool isEnabled, Action<bool> trigger) { } | |
} | |
public class Button : View | |
{ | |
public Button(string text, Action trigger) { } | |
} | |
public class Slider : View | |
{ | |
public Slider(int step, int through, Action<int> onEditingChanged) { } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment