Retro MVC DualListBox

Met de komst van Asp.Net MVC is er een duidelijke verschuiving zichtbaar
geworden. Er worden meer webapplicaties ontwikkeld met het gebruik in een
webbrowser in gedachte (..). De afgelopen vijftien jaar (1995-2010) zijn veel
websites opgezet met de gedachte dat deze zich als een client-server applicatie
moesten gedragen.

Client-Server was daarvoor zeer succesvol en is dat ook nog een tijd
gebleven. Maar de functionele wensen voor de webapplicaties werden opgesteld met
CS in gedachte. Dit was wat de gebruikers kenden en wat functionele ontwerpers
kenden en wat requirement engineers kenden.

Dual pist

Client-Server was ook de gouden tijd voor de gebruiker. De applicaties
draaiden op de PC, waren razend snel, er was volop state en interactie en de
fraaiste controls werden ontworpen of ingekocht.

Met dit in gedachte stortte een heel leger ontwikkelaars zich op het web,
aangemoedigd met hun CS specificatie of zelfs alleen maar CS kennis. Microsoft
deed daar nog een schepje bovenop met Asp.Net webforms want hiermee waren de schermen ook eenvoudig bij elkaar te klikken.

Veel webapplicaties zijn dus zo gebouwd zonder zicht op de grootte van de
schermen en de bijbehorende complexiteit. De ViewState kon alles aan en desnoods werd JavaScript als plakband toegepast om de schermen aan elkaar te plakken.

Maar nu hebben we al weer de derde versie van Asp.Net MVC.

Inmiddels heeft het zich bewezen als een blijvertje. En dat terwijl er in
eerste instantie zoveel weerstand was:

– “Waar moet ik dan mijn controls plaatsen?”

– “Gewoon, die typ je uit in de html in de view”

– “Wat? Html, moet ik die zelf gaan schrijven?”

Vorig jaar ben ik ook overgestapt naar MVC en eigenlijk ik wil niet meer
terug. Ik voel me productief bij het bouwen van logica en de werking van de
applicatie is heel goed te traceren in de verschillende onderdelen van een
project. Ook als ik bij project betrokken raak waar ik zelf niet aan gebouwd
heb, blijk ik heel snel mijn weg te vinden in de logica en zijn ‘smells’ direct zichtbaar.

Toch kreeg ik vorige week weer een Déjà vu. Er moest een keuzelijst komen en
dit kon niet met een simpele checklistbox opgelost worden. Dit is het standaard
scenario voor het onderhouden van een N op M relatie waarbij uit een lijst van
te kiezen waarden een deelverzameling gekozen moet worden. Automatisch dacht ik aan een dual listbox want ik ben ook maar een eenvoudige ontwikkelaar,
opgegroeid in het CS tijdperk. “Als je alleen maar een hamer hebt, is de hele
wereld een spijker” zullen we maar zeggen.

Dus snel even een MAKE OR BUY beslissing genomen en gezien de fragmentarische hits op het web werd het… make. Overigens pleit ik voor “buy or make” want hoewel make heel belangrijker is, is buy veel belangrijker want goedkoper (..).

Dus ik heb wat bij elkaar gesprokkeld en zelf onderstaande implementatie
geschreven. Deze is niet helemaal feature complete (oa. sorteren na oversluizen
van een selectie en de onvermijdelijke drag-and-drop) maar ik vind dat het heel
aardig werkt. Ik kan deze dual listbox  prima in insert en edit views toepassen
en vereisten voor mijn viewmodel zijn minimaal.

Dus laten we eerst naar de DualListViewModel kijken. Hierin is een lijst met
alle keuzes opgenomen en een lijstje van unieke sleutels die de huidige selectie
voorsteld (in het geval van een update scenario):

using System.Collections.Generic;namespace MvcApplication.Controllers
{
  public class DualListViewModel
  {
    public List<int> SelectedIds { get; set; }
    public List<DualListSelectionItem> Selectables { get; set; }
  }

  public class DualListSelectionItem
  {
    public int ID { get; set; }
    public string Name { get; set; }
  }
}

Dit was een inkoppertje. De controller moet dus twee lijstjes gaan
aanleveren. Natuurlijk de lijst van alle mogelijke keuzes en eventueel dus die
lijst met de huidige keuze. Dit is in de volgende Controller ingevuld:

using System;using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcApplication.Controllers
{
  public class DualListController : Controller
  {
    public ActionResult Edit(int id)
    {
      return View(new DualListViewModel
                    {
                     SelectedIds = new List<int> { 15 },
                     Selectables = GetSelectables(),
                    });
    }
    [HttpPost]
    public ActionResult Edit(DualListViewModel model)
    {
      model.SelectedIds = model.SelectedIds ?? new List<int>();
      // start stub for save
      List<DualListSelectionItem> allselectionItems = GetSelectables();
      model.Selectables = GetSelectables();
      // end stub for save
      return View(model);
    }

    private List<DualListSelectionItem> GetSelectables()
    {
      var result = new List<DualListSelectionItem>();
      // Stub for getting the current selectables
      // (without any selected)...
      result.Add(new DualListSelectionItem { ID = 1, Name = "aaa", });
      result.Add(new DualListSelectionItem { ID = 21, Name = "zzz", });
      result.Add(new DualListSelectionItem { ID = 13, Name = "ccc", });
      result.Add(new DualListSelectionItem { ID = 144, Name = "ddd" });
      result.Add(new DualListSelectionItem { ID = 16, Name = "ttt", });
      result.Add(new DualListSelectionItem { ID = 231, Name = "fff", });
      result.Add(new DualListSelectionItem { ID = 123, Name = "kkk", });
      result.Add(new DualListSelectionItem { ID = 154, Name = "iii", });
      result.Add(new DualListSelectionItem { ID = 15, Name = "bbb", });
      return result;
    }
  }
}

Wat we zien is twee actions voor een Edit view. De eerste zal de Edit view
gevuld gaan tonen. Hier negeer ik de doorgegeven primary key en verzin dat ik
waarde 15 (bbb) al vooraf gekozen heb. De methode GetSelectables levert mij de
complete lijst van keuzes aan, dus ook diegene die al gekozen zijn. In de view
zal dit uit elkaar getrokken worden.

Bij de opslag van de Edit view zal de post action uitgevoerd worden. Mocht de
gebruiker geen enkel item kiezen dan zal de property SelecedIds null zijn. Ik
forceer dat dit een lege lijst is want dan krijg ik geen last van null reference
exceptions…

Om te bewijzen dat ik de uiteindelijke selectie correct kan oppakken in de
post-back, toon ik nogmaals dezelfde view maar nu met de nieuwe selectie. De
nieuwe selectie blijft ook geldig als het viewmodel niet aan alle validaties
voldoet. Dit is wel zo handig.

Blijft over de View. Hierin zijn twee HTML multiselectlists opgenomen. De
rechter toont de huidige selectie en de linker toont de items die nog
geselecteerd kunnen worden. Daar tussen staan knoppen om één of meerdere items over te zetten:

<%@ Page Title="" Language="C#"
 MasterPageFile="~/Views/Shared/Site.Master"
 Inherits="System.Web.Mvc.ViewPage<MvcApplication.Controllers.DualListViewModel>"
 %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Edit DualList
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Edit DualList</h2>
<% using (Html.BeginForm()) { %>
<%: Html.ValidationSummary(true) %>
<fieldset>
<legend>DualListViewModel</legend>
<table>
<tr style="border:0">
<td style="border:0; vertical-align:top;text-align:left">
Selectable:<br />
<%= Html.ListBox("Selectables",
new MultiSelectList(Model.Selectables.Where(x =>
 !Model.SelectedIds.Contains(x.ID)).OrderBy(x => x.Name), "ID", "Name"), new {
 style = "width:200px; height:120px" })
%>
</td>
<td style="border:0; vertical-align:middle;
 text-align:center">
<input type="button" value=">"
 onclick="append('#Selectables', '#SelectedIds');" style="width:25px" /><br
 />
<input type="button" value=">>"
 onclick="appendAll('#Selectables', '#SelectedIds');" style="width:25px" /><br />
<input type="button" value="<"
 onclick="remove('#Selectables', '#SelectedIds');" style="width:25px" /><br />
<input type="button" value="<<"
 onclick="removeAll('#Selectables', '#SelectedIds');" style="width:25px" />
</td>
<td style="border:0; vertical-align:top;
 text-align:left">
Selected:<br />
<%= Html.ListBoxFor(
x => x.SelectedIds,
new
 MultiSelectList(Model.Selectables.Where(x =>
 Model.SelectedIds.Contains(x.ID)).OrderBy(x => x.Name), "ID", "Name"), new {
 style = "width:200px; height:120px" })
%>
</td>
</tr>
</table>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
</asp:Content>

Wat duidelijk moet zijn is dat de rechter lijst gebind is aan het model,
namelijk aan de SelectedIds property. De vulling van de twee lijsten sluiten
elkaar uit door handig met de fluent notatie van LinQ te spelen.

Als laatste moet het overzetten van de items met JavaScript ondersteund
worden. Hieronder staat mijn rolletje plakband:

<script language="javascript" type="text/javascript">
$().ready(function () {
  $("#Selectables option").attr("selected", "");
  $("#SelectedIds option").attr("selected", "");
});

$("form:first").submit(function () {
  $("#SelectedIds option").attr("selected", "selected");
});

function append(selectables, selectedIds) {
  $(selectables + " option:selected").appendTo(selectedIds);
}

function appendAll(selectables, selectedIds) {
  $(selectables + " option").appendTo(selectedIds);
}

function remove(selectables, selectedIds) {
  $(selectedIds + " option:selected").appendTo(selectables);
}

function removeAll(selectables, selectedIds) {
  $(selectedIds + " option").appendTo(selectables);
}
</script>

Ik maak gebruik van JQuery om relaxed met JavaScript te spelen. Na het laden
van het scherm voer ik twee commando’s uit om eventuele selecties uit een
roundtrip op te heffen.

Vervolgens forceer ik dat als het (eerste) form in de edit view gesubmit
wordt, alle tot dan toe geselecteerde items in de rechter listbox ook als
selected te boek staan. Items die rechts staan maar niet ‘selected’ worden niet
aan de controller doorgegeven als geselecteerd!

Daarna komt nog de verplichte code voor de view knoppen. Deze zou nog
uitgebreid kunnen worden met een nieuwe sorting op de listbox waar de items aan toegevoegd zijn.

Hiermee is de duallistbox een feit. Zie hier het resultaat:

MVC DualList

Het bouwen van deze retro dual list kostte me een avondje maar ik vind het
welbesteed. Ik heb nu een lichtgewicht oplossing die heel intuïtief overkomt,
altans voor de gebruikers met een client server achtergrond…

Zo nu snel weer verder met Pong!