I den förra postningen om MVC på mobilen så introducerade jag ett MVC-ramverk utvecklat av Alex Yakhnin på Microsoft. Här tänkte jag fortsätta utvärdera ramverket genom att göra lite mera nytta än bara visa ett formulär. Den här gången kommer jag att skapa en TODO-lista som kommer att se ut som formuläret till höger. Interaktionen är på det sättet att användaren skriver text i textrutan högst upp och kan välja att antingen trycka enter eller helt enkelt välja Add-knappen för att lägga till texten som en uppgift i listan. Senare kommer jag att lägga till interaktion för att ta bort uppgifter och kanske till och med hantera prioriteringar.
Jag kan utgå från den förra artikelns resultat och helt enkelt lägga till tre kontroller på formuläret (och ta bort den befintliga “labeln”):
- En TextBox med namnet txtTodoToAdd
- En knapp med namnet btnAdd
- En ListBox med namnet lstTodos
Jag byter också namn på formuläret till att heta TodosView samt sätter texten på formuläret att vara “TODO”. För att vara lite förebyggande så kommer jag att använda ett Repository för att hantera uppgifter så jag skapar helt enkelt ett interface enligt följande:
public interface ITodoRepository
{
IEnumerable<string> GetAll();
void Add(string task);
}
Jag skulle naturligtvis kunna använda en databas eller annat persistent lagringskälla, men nu vill jag fokusera på ytan så jag implementerar ITodoRepository i ett “fake”-objekt enligt följande:
public class TodoRepositoryFake : ITodoRepository
{
private List<string> _todos = null;
public TodoRepositoryFake()
{
_todos = new List<string>
{
"Buy milk",
"Pick up kids"
};
}
public IEnumerable<string> GetAll()
{
return _todos.AsEnumerable();
}
public void Add(string todo)
{
_todos.Add(todo);
}
}
Jag ser också till att registrera detta repository i min ApplicationManager-klass enligt följande:
private static void Initialize()
{
container = new Container();
container.Register<ITodoRepository>(c => new TodoRepositoryFake());
...
Nu har jag kommit till att skapa min riktiga “controller”, den får heta TodosController och kommer att ärva från den generiska basklassen Controller<IEnumerable<string>> för att ge indikation på att vi kommer att skicka en samling av strängar som primär källa för data till vyn. Det ger nämligen att fältet this.View.ViewData.Model kommer att starkt typas till att vara just IEnumerable<string>.
public class TodosController : Controller<IEnumerable<string>>
{
ITodoRepository _repository = null;
public TodosController(IView<IEnumerable<string>> view) : base(view) { }
public Container Container { get; set; }
private void OnExit(object sender, EventArgs e)
{
Application.Exit();
}
}
Det första jag vill ska hända när controllern laddas är att den instansierar mitt repository och hämtar alla uppgifter samt skickar dessa till vyn. En metod som lämpar sig för detta ändamål är OnInitialize-metoden som kan hittas på basklassen Controller. Därför implementerar jag metoden enligt följande:
protected override void OnInitialize(params object[] parameters)
{
_repository = this.Container.Resolve<ITodoRepository>();
this.view.ViewData.Model = _repository.GetAll();
}
Men hur får jag då min vy att förstå att jag har laddat och populerat ViewData.Model med data? Jo, helt enkelt genom att skicka en händelse till vyn och berätta detta. Därför skapar jag en EventHandler och publicerar den till vyn på samma sätt som jag i förra artikeln publicerade en händelse från vyn till controllern.
[PublishEvent("OnTodosLoaded")]
public event EventHandler TodosLoadedEvent;
Sedan måste jag “trigga” den händelsen och det görs vanligtvis med följande kodrader:
if (TodosLoadedEvent != null)
{
TodosLoadedEvent(this, EventArgs.Empty);
}
Just det mönstret är något som jag har kommit att använda ofta när jag använder det här ramverket därför har jag skapat några “extensions-methods” som underlättar anropet lite (mina extension-methods kan du hitta sist i den här artikeln), koden blir då som följer istället:
this.RaiseEvent(TodosLoadedEvent, EventArgs.Empty);
Så den slutliga koden på OnInitialize blir som följer:
[PublishEvent("OnTodosLoaded")]
public event EventHandler TodosLoadedEvent;
protected override void OnInitialize(params object[] parameters)
{
_repository = this.Container.Resolve<ITodoRepository>();
this.view.ViewData.Model = _repository.GetAll();
this.RaiseEvent(TodosLoadedEvent, EventArgs.Empty);
}
Nu vill jag också skapa logiken i vyn för att hantera uppgifter samt lyssna på händelsen att repositoryt har laddat uppgifterna. Min vy som alltså heter TodosView ärver från ViewForm samt implementerar också interfacet IView<IEnumerable<string>>, som precis som på controllern gör att ViewData.Model starkt typas till det som jag vill använda mig av. Koden som följer har också redan implementerat händelsen för att avsluta applikationen precis som den tidigare artikeln:
public partial class TodosView : ViewForm, IView<IEnumerable<string>>
{
[PublishEvent("OnExit")]
public event EventHandler OnExitEvent;
public TodosView()
{
InitializeComponent();
}
#region IView<IEnumerable<TodoTask>> Members
public new IEnumerable<string> Model { get; set; }
public new ViewDataDictionary<IEnumerable<string>> ViewData { get; set; }
#endregion
private void menuExit_Click(object sender, EventArgs e)
{
this.RaiseEvent(OnExitEvent, EventArgs.Empty);
}
}
Sedan vill jag lägga till logiken för att visa de laddade uppgifterna i listan på vyn:
private void OnTodosLoaded(object sender, EventArgs e)
{
this.listTodos.Items.Clear();
foreach (var item in this.ViewData.Model)
{
this.listTodos.Items.Add(item);
}
this.listTodos.Refresh();
}
Den ovanståend metoden kommer alltså av ramverket kopplas som en metod som anropas när controllern triggar händelsen OnTodosLoaded som publiceras med attributet [Publish(…)] i controllern. Så långt fungerar applikationen som den ska men jag vill också ha logiken för att kunna lägga till nya uppgifter så jag kopplar händelse-hanterare till händelsen KeyDown på txtTodoToAdd och till händelsen Click på btnAdd. Koden skriver jag som nedan:
[PublishEvent("OnTodoAdded")]
public event EventHandler<DataEventArgs<string>> OnTodoAddedEvent;
private void txtTaskToAdd_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
this.RaiseEvent(OnTodoAddedEvent,
new DataEventArgs<string>(txtTaskToAdd.Text));
txtTaskToAdd.Text = "";
}
}
private void btnAdd_Click(object sender, EventArgs e)
{
this.RaiseEvent(OnTodoAddedEvent,
new DataEventArgs<string>(txtTaskToAdd.Text));
txtTaskToAdd.Text = "";
}
Observera att jag också har skapat ytterligare en publicerad händelse som kallas OnTodoAdded som notifierar kontrollern att jag har lagt till en ny uppgift. När jag triggar den händelsen i båda hanterarna så skickar jag också med en sträng som beskriver den nya händelsen. Det är också därför som den generiska EventHandlern har typ-defintionen: EventHandler<DataEventArgs<string>> OnTodoAddedEvent. DataEventArgs<T> kommer från System.Mobile.Mvc-namnrymden.
Sista delen i applikationen blir att i controllern hantera tillägget av en ny uppgift, spara i repositoryt och sedan notifiera vyn om att samlingen är uppdaterad. Följande metod läggs till i controllern, och det naturligtvis viktigt att metoden heter det som det publicerade attributet specificerat i vyn:
private void OnTodoAdded(object sender, DataEventArgs<string> e)
{
_repository.Add(e.Value);
this.view.ViewData.Model = _repository.GetAll();
this.RaiseEvent(TodosLoadedEvent, EventArgs.Empty);
}
Jag gör det ganska enkelt för mig genom att helt enkelt lägga till uppgiften som skickas med hjälp av DataEventArgs<T>-parametern e i repositoryt och sedan ladda alla uppgifter på nytt innan jag notifierar vyn på samma sätt som tidigare att datat har uppdaterats.
Nu måste jag bara uppdatera ApplicationManager att registrera vyn och controllern innan jag kör applikationen. Här är den uppdaterade initialiserings-koden:
private static void Initialize()
{
container = new Container();
container.Register<ITodoRepository>(c => new TodoRepositoryFake());
container.Register<TodosView>(c => new TodosView());
container.Register<TodosController>(
c => new TodosController(
c.Resolve<TodosView>())).InitializedBy(
(c, v) => v.Container = c);
Navigator.SetControllerProvider(new ControllerProvider(container));
}
Och i Main-metoden uppdaterar jag koden att bli följande:
public static void Start()
{
var todosController = container.Resolve<TodosController>();
todosController.Initialize();
Application.Run(todosController.View as Form);
}
Prova att köra!
Mina extension-methods:
Här följer fyra metoder som kan användas för att notifera vyer och controllers att det ar hänt något som bör trigga händelser i respektive:
public static class MyExtensions
{
public static void RaiseEvent(
this ViewForm form,
EventHandler theEvent,
EventArgs e)
{
if (theEvent != null)
{
theEvent(form, e);
}
}
public static void RaiseEvent<TEventArgs>(
this ViewForm form,
EventHandler<TEventArgs> theEvent,
TEventArgs e)
where TEventArgs : EventArgs
{
if (theEvent != null)
{
theEvent(form, e);
}
}
public static void RaiseEvent(
this Controller form,
EventHandler theEvent,
EventArgs e)
{
if (theEvent != null)
{
theEvent(form, e);
}
}
public static void RaiseEvent<TEventArgs>(
this Controller form,
EventHandler<TEventArgs> theEvent,
TEventArgs e)
where TEventArgs : EventArgs
{
if (theEvent != null)
{
theEvent(form, e);
}
}
}
Kom gärna med kommentarer!