Modelgedreven ontwikkelstraat in .NET (7): Objectbrowser

 
28 december 2008

Deze post ga ik wat dieper in op user interface logica, in dit geval een typisch Windows Forms geval; browsertjes / gridjes met objecten.

Vaak wordt in de Dotnet wereld databinding gebruikt om lijsten van objecten in grids te tonen. Dat werkt prima, als je tot een paar duizend items in je lijst hebt zelfs uitstekend. In echte praktijksituaties echter voldoet databinding vaak niet meer. Ik kies er dan ook voor om een browsbare lijst te maken van Employees (zie eerdere posts voor het model) met 1 miljoen objecten. Daar is niet tegenaan te databinden, dat verzeker ik je :).

Een hiervoor geschikte strategie is om je datagrid in zgn. virtual mode te gebruiken. Het grid pakt dan pas gegevens uit de datasource op moment dat dat nodig is, dus als naar een bepaalde positie toegescrolld is worden pas de gegevens van die positie opgevraagd. Dit voorbeeld illustreert tevens de kracht van het template gebaseerd code genereren dat ik in deze modelgedreven ontwikkelstraat toepas; er is veel code nodig om deze virtual mode te implementeren.

Ik begin maar gewoon met wat voorbeeldcode van een specifiek gridje voor Employees.

partial class EmployeeBrowser : System.Windows.Forms.DataGridView
{
    EmployeeRepository repository = new EmployeeRepository();
     public EmployeeBrowser()
        : base()
    {
        this.VirtualMode = true;
        this.Columns.Add("Id", "Id");
        this.Columns.Add("Name", "Name");
        this.CellValueNeeded += new DataGridViewCellValueEventHandler(Browser_CellValueNeeded);
        this.RowCount = repository.Count;
        repository.CachefetchingStart += new EventHandler(repository_CachefetchingStart);
        repository.CachefetchingDone += new EventHandler(repository_CachefetchingDone);
    }
    void repository_CachefetchingDone(object sender, EventArgs e)
    {
        Cursor.Current = Cursors.Arrow;
    }
    void repository_CachefetchingStart(object sender, EventArgs e)
    {
        Cursor.Current = Cursors.AppStarting;
    }
    void Browser_CellValueNeeded(object sender, DataGridViewCellValueEventArgs e)
    {
        if (repository[e.RowIndex] != null)
            e.Value = repository[e.RowIndex].GetValueForIndexedProperty(e.ColumnIndex);
        else
            e.Value = null;
    }
}

Dat is – zoals je ziet – vrij simpel. Een paar interessante fragmenten zijn te vinden in de constructor. Allereerst zie je dat ik plat zelf mijn kolommen toevoeg in code. Da’s in dit geval gewoon een kwestie van genereren. Daarnaast wordt er een drietal eventhanders gewired; twee voor cachefetching events (start en einde), daarmee kan ik de gebruiker van feedback voorzien over de status van de applicatie (in dit geval via de cursor).
Een interessantere is de CellValueNeeded handler. Hier zien we dat we DataGridViewCellValueEventArgs binnenkrijgen met een kolom en een rijnummer. Ik heb de GetValueForIndexedProperty() methode gegenereerd voor elke domain class. De properties zoals weergegeven in volgorde in mijn model komen zo ook in het gridje te staan. Hier zien we de implementatie van de Employee class:

public object GetValueForIndexedProperty(int index)
{
    if (index == 0)
        return this.id;
    if (index == 1)
        return this.Name;
    return null;
}

Je ziet al, heel platte code, prima te genereren dus.

Zoals je al zag heb ik mijn grid gekoppeld met een object van type EmployeeRepository. Dit is een gedeeltelijke IList implementatie die o.a. de geindexeerde toegang in de lijst en de Count implementeert. Dit maakt dat ik deze implementatie heel makkelijk kan vervangen door een andere.

Om te beginnen de geindexeerde toegang implementatie van de EmployeeRepository:

public Employee this[int index]
{
    get
    {
        if (localcache.ContainsKey(index))
            return localcache[index];
        else
        {
            AddToCache(index, Get(index));
            if (localcache.ContainsKey(index))
                return this[index];
            else
                return null;
        }
    }
    set
    {
        throw new NotImplementedException();
    }
}

Geen rocket science hier. Enige dat ik hier heb ingebouwd is een configureerbare cache in de vorm van een dictionary die virtueel de hele tabel bevat die in de database zit. Als er een cache ‘miss’ is wordt er een Get() op de betreffende index uitgevoerd. De Get() methode is waar de feitelijke data uit de database gelepeld wordt:

private List Get(int startindex)
{
    if (CachefetchingStart != null)
        CachefetchingStart(this, null);
 
    OdbcCommand cmd = new OdbcCommand(
        @"
		With tmp AS
		(SELECT *,
			ROW_NUMBER() OVER (order by name) as RowNumber,
			(select count(*) from Employee) as TotalRows
		    FROM Employee)
			select *
			from tmp
			Where RowNumber Between " + startindex + " and " + (startindex + Constants.CacheSize), connection);
 
    List items = new List();
 
    DataTable dt = new DataTable();
    OdbcDataAdapter da = new OdbcDataAdapter(cmd);
    da.Fill(dt);
 
    foreach (DataRow row in dt.Rows)
    {
        Employee instance = new Employee();
        instance.id = (Guid)row["id"];
        if (row["name"] != DBNull.Value)
            instance.name = (String)row["name"];
        items.Add(instance);
    }
 
    if (CachefetchingDone != null)
        CachefetchingDone(this, null);
 
    return items;
}

Hier zien we allereerst dat een event opgegooid wordt dat we beginnen met een cache fetch. Dat hadden we al afgevangen d.m.v. de cursor die veranderd hierboven. Daarna gaan we een hele vreemde query uitvoeren; eentje die het MySQL LIMIT statement een beetje probeert te na-apen. Meer hierover in deze post op mijn eigen blog.
Platweg wordt deze recordset vertaald naar een lijst met objecten. Voor de volledigheid toon ik nog even de AddToCache() methode, die na een overschrijding van 5x de fetchgrootte zichzelf leeggooit. Dit om te voorkomen dat je op een gegeven momenten een miljoen Employees in je geheugen hebt zitten. (voor de geinteresseerde: uiteraard heb ik dat geprobeerd. Taskmanager gaf aan dat het geheugengebruik van mijn applicatie voor 1 miljoen Employees naar 350 MB toeging).

private void AddToCache(int startindex, List items)
{
    if (localcache.Count > 5 * Constants.CacheSize)
        localcache.Clear();
    for (int i = startindex; i < startindex + items.Count; i++)
       localcache[i] = items[i - startindex];
}

Altijd handig om de boel in zijn geheel te bekijken: hier te downloaden. De database genereert vanzelf een miljoen Employees via de knop (duurt wel een minuutje of 2 :)).


Werken met ?
Kijk dan bij onze mogelijkheden voor starters en/of ervaren IT'ers.


Categorieën: Development

Tags: , ,


Reacties (2)

  • Hoi Sven,

    Wellicht dat dit voor jou ook handig is. Ik heb de oplossing ook losgetrokken van het hele modelgedreven verhaal op mijn eigen blog:
    http://whiletrue.nl/blog/?p=85

    Dat werkt op elke visual studio 2008, ook zonder DSL tools enzo.

    Geplaatst op 04 januari 2009 om 15:37 Permalink

  • Sven Landgraf schreef:

    Wow cool!
    Dit kan ik goed ook gebruiken, maar dan met Active Directory als DataSource. Heb nu de source gedownload.

    Oh, en natuurlijk Happy 2009 iedereen!
    Greetz Sven

    Geplaatst op 03 januari 2009 om 23:50 Permalink