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!

Implementatie Telerik MVC Extensions TreeView

Hoewel  met de 3e versie van Asp.Net MVC er weer een aantal HTML
helpers bijgekomen zijn om data uitdagend te representeren, is er nog geen
standaard TreeView.

Gelukkig heeft Telerik een aantal aantrekkelijke extensions voor noppes in de
aanbieding en daartussen zit ook een TreeView.

tree view

Ik moest voor een project een TreeView implementeren en wilde hiervoor de
bewuste Telerik oplossing toepassen. Helaas besteedt Telerik minder aandacht aan hun documentatie en training-materiaal dan aan de kwaliteit van hun extensions. Gelukkig kreeg ik flinke steun van Telerik’s Phil Japikse en Carl Bergenhem. Omdat het eigenlijk redelijk eenvoudig is om deze HTML helper toe te passen heb ik hier een korte introductie samengesteld.

Let op: De Telerik extensions die via Nuget te benaderen zijn, werken niet
(direct) met MVC2 . Deze verwijzen graag naar MVC3.

We gaan eerst de TreeView vullen via ajax callbacks.

Dus start een nieuw Asp.Net MVC3 project:

add mvc3 project

Voeg hier direct de Telerik MVC Extensions aan toe via onvolprezen Nuget
manager:

Nuget

Op naar de site.master. Voeg daar binnen de Head net onder de jquery link toe:

<%Html.Telerik().StyleSheetRegistrar().DefaultGroup(group => group.Add("telerik.common.css")
  .Add("telerik.web20.css"))
  .Render();
%>

En voeg net voor het einde vande </body> toe:


<%= Html.Telerik().ScriptRegistrar() %>

Dit maakt ons project klaar het het toepassen van de Telerik Extensions.

De telerik extensions zijn als het goed is, al bekend binnen de ASPX
viewengine. De namespace is inmiddels toegevoegd aan de web.config van ons
project.

Maar bij MVC3 is er ook de Razor viewengine. daarvoor moet de namespace
referentie nog even opgelost worden. Voeg dan  <system.web.webPages.razor>
toe binnen de web.config in \views\Shared.

Dan zijn we nu eindelijk toegekomen aan de toepassing van de TreeView.

Eerst gaan we naar de HomeController. We hebben een Backend methode nodig om steeds een lijst van child treeviewitems op te hoesten. Deze wordt via een ajax callback steeds aangeroepen als we een node openklappen:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult _AjaxTreeviewLoading(TreeViewItem node)
{
  // create the same over and over... just for fun 🙂
  IEnumerable nodes =
    new List<TreeViewItem>{
      new TreeViewItem
        {
           Text = "Name",
           Value = "1",        // not really unique 😉
           LoadOnDemand = true,
           Enabled = true
        }};

   return new JsonResult { Data = nodes };
}

Deze methode spuuwt steeds dezelfde node uit maar hier kan ook een nette
lijst opgegeven worden.

Maak uiteindelijk onderstaande wijziging  onderaan de \Home\Index view:

<%= Html.Telerik().TreeView().Name("TreeView").ExpandAll(true)
    .ClientEvents(events => events
    .OnSelect("onSelect"))
    .DataBinding(dataBinding => dataBinding
    .Ajax().Enabled(true).Select("_AjaxTreeviewLoading", "Home"))
%>
<p>
<%: Html.ActionLink("Create New", "Create") %>
|
<a id="edit" href="#">Edit</a>
|
<a id="delete" href="#">Delete</a>
</p>

<script type="text/javascript">
  $(document).ready(function () {
    $('#edit').attr('disabled', true);
    $('#delete').attr('disabled', true);
  });

  function onSelect(e) {
    var id =
      $('#TreeView').data('tTreeView').getItemValue(e.item);

    $('#edit').attr('href', 'Home/edit/' + id).attr('disabled', false);

    $('#delete').attr('href', 'Home/delete/' + id).attr('disabled', false);
  }
</script>

Hier wordt de Telerik MVC TreeView toegevoegd en gebind aan een ajax Call.
Tevens is er een Insert, Edit en een Delete link.

Dit ziet er dan zo uit:

Treeview loaded by ajax callback

Iedere node is een aparte ajax call naar de server. Deze geeft dus steeds een
nieuwe node terug als een enkele child. Dit kan wel even doorgaan…

Ook is een ClientEvent van de TreeView gekoppeld aan de OnSelect functie. De
laast aangeklikte child wordt zo ‘onthouden’ via het onselect event. Hierdoor
krijgen de twee links naar Edit en Delete  steeds een andere referentie.

Dit is een aardige oplossing maar als de treeview erg groot wordt, komen de
links in de verdrukking.

Mijn voorkeur is een aparte Edit en Delete action link per node, net als bij
een regulier html tabel. Dit zou dan opgelost moeten worden met een soort
template per node.

Gebruik van templates is helaas geen optie binnen dit kader: “Now the issue
that he was experiencing below is due to the fact that we do not really have a
Template functionality implemented in the TreeView component quite yet” aldus
Phil.

Het alternatief is het gebruik van een Telerik MVC TreeView in combinatie met
een ViewModel.

Eerst defineren we een class welke binnen het ViewModel een node moet gaan
representeren:

public class Customer
{
  public int CustomerID { get; set; }
  public string Text { get; set; }
  public List<Customer> Children { get; set; }
}

Vervolgens geven we een lijst van deze objecten door in de index methode van
de home controller:

public ActionResult Index()
{
  ViewBag.Message = "Welcome to ASP.NET MVC!";
  IEnumerable nodes =
newList<Customer>{
new Customer {CustomerID = 22
                    ,Text = "aaa"
                    , Children = new List<Customer>()
                   },
      new Customer {CustomerID = 23
                    ,Text = "ccc"
                    , Children = new List<Customer>
                      {
                         new Customer {CustomerID = 25
                                       ,Text = "eee"
                                       , Children = new List<Customer>()
                                      },
                         new Customer {CustomerID = 26
                                       ,Text = "hhhh"
                                       , Children = new List<Customer>()
                                      },
                      }
                   },
      new Customer {CustomerID = 24
                    ,Text = "bbb"
                    , Children = new List<Customer>()
                   }
                };

  return View(nodes);
}

Merk op dat de children hier hardcoded toegevoegd worden. Dit kan later ook
via een recursieve methode opgelost worden. Merk ook op dat de kinderen niet
direct deel uitmaken van de IEnumerable lijst.

Als laatste moet de Index view bijgewerkt worden. We gebruiken hiervoor een
nieuwe treeview helper definitie:


<%= Html.Telerik().TreeView()
.Name("TreeViewByViewModel")
.BindTo(Model,
mappings =>       {
mappings.For<MvcApplication10.Controllers.Customer>(
binding => binding.ItemDataBound(
                      (currentItem, currentCustomer) => {
      currentItem.Text =
currentCustomer.Text +
String.Format(" {0} {1}",
Html.ActionLink("Edit", "Edit", new {
            id = currentCustomer.CustomerID }),
Html.ActionLink("Delete","Delete", new {
            id = currentCustomer.CustomerID }));

currentItem.Encoded = false;

currentItem.Expanded = true;

currentItem.Value = currentCustomer.CustomerID.ToString();
      })
      .Children(currentCustomer => currentCustomer.Children)
);
})
%>

Hierbij wordt de text van iedere node gevuld met een omschrijving en met twee
url’s. Dit wordt geforceerd door de ‘Encoded’ property op false te zetten.

En de children wordt zichtbaar door te refereren naar de Children property
van de Customer.

De TreeView notatie is verre van leesbaar en erg gevoelig voor het tonen van
verkeerde foutmeldingen als er ook maar één karakter verkeerd staat. Let dus
goed op de notatie.

Dit resulteert uiteindelijk in een fraaie treeview met bij iedere node een
link voor de edit en de delete actions.

Treeview loaded by viewmodel and with template

Conclusie: hoewel Telerik HTML helpers relatief eenvoudig toe te passen zijn,
maakt de ingewikkelde notatie deze tot een waar misterie. Maar met wat
doorzettingskracht en een heleboel surfen blijkt dat deze HTML Helpers eigenlijk erg fraaie resultaten geven.