Some words about the Gridview.RowDataBound-event

A common situation when using a GridView in an Asp.NET-application is that you might want to manipulate the appearance on some cell based on the value of that cell, for example displaying negative numbers in a different color. Now, I couldn't find any negative numbers in the Northwind sample database so here is my example;

CropperCapture[1] 

This is a product list from the Northwind Database and the goal here is to change the text color of the product name to red if the product is discontinued. To do this we will implement the RowDataBound-event of the GridView. The eventhandler has the following signature;

protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)

Through the EventArgs object e you can now access a property named Row of type GridViewRow that lets you manipulate and access the row that was just databound. Now, here is what you don't do; to read the values in the row you could access the Cells-collection of the row. This example would read the UnitPrice-value and assign it to the foo variable;

string foo = e.Row.Cells[2].Text; //Don't do this

However, this isn't very good since rearranging the columns would break the code, and in this example it would be even more awkward since we would have to access the checkbox control in the last cell and read it's Checked property. What we want is a way to read each value through the corresponding data source column name, i.e. "ProductName", "QuantityPerUnit" etc. Of course it would be nice if we in the code snippet above could write e.Row.Cells["UnitPrice"].Text but this is not possible, instead we will make use of the e.Row.DataItem object;

DataRowView dataItem = (DataRowView)e.Row.DataItem;
if ((bool)dataItem["Discontinued"] == true)
{
    //Do something
}

The DataItem property is of type Object, so we need to cast it to the right type before we can access the values. In this case (where the gridview is bound to an SqlDataSource) the correct type is DataRowView. (More on this here.)

After the cast we can access the columns by indexing the dataItem object with the proper string value. However, running this code will serve us the dreaded "Object reference not set to an instance of an object." error message;

CropperCapture[4]

This is due to the fact that the RowDataBound is raised whenever a row is databound, which also applies to the header row of the gridview. When the header row is bound, e.Row.DataItem will be null because the header row does not reference one of the posts in the data source. So we need to wrap it with the following if statement (bold);

if (e.Row.RowType == DataControlRowType.DataRow)
{
    DataRowView dataItem = (DataRowView)e.Row.DataItem;
    if ((bool)dataItem["Discontinued"] == true)
    {
        //Do something
    }
}

Now we could replace the comment above with the following to make the product name red;

e.Row.Cells[0].ForeColor = Color.Red; //Don't do this

But now we are back to the problem with referencing cells through a hard coded index number, so if the columns are rearranged it might not be the product name that turns red. I googled this a bit and found this and this, but I can't figure out a really good solution to this. The problem with the first link is that looping through the Columns collection that is objects of type DataControlField does not give you a secure access to the data source column name nor the column index. Through the e.Row.DataItem.Row.Table you can access DataColumn objects through which you can access the propertys ColumnName and Ordinal (column position), but it seems that this table object stays true to the column order of the datasource and not the column order in the gridview. Another try at google led me to this hack, which of course is better than hardcoding numbers but still not what I really want. After reading this forum thread i decided to give up, use the hack idea, but instead of using the AccessibleHeaderText property use the SortExpression property since that is almost always the same as the data source column name (if sorting isn't done on multiple columns) and it is automatically set in the aspx markup when working in the design view so no extra work is needed.

So, the previous code snippet will be replaced with this;

int cellIndex = 0;
for (int i = 0; i < gvProducts.Columns.Count; i++)
{
    if (gvProducts.Columns[i].SortExpression == "ProductName")
    {
        cellIndex = i;
        break;
    }
}
e.Row.Cells[cellIndex].ForeColor = Color.Red;

And after some refactoring making use of the C# 3.0 Extension Method feature you get this reusable method;

public static class Extensions
{
    public static int GetColumnIndex(this GridView gv, string columnName)
    {
        int result = -1;
        for (int i = 0; i < gv.Columns.Count; i++)
        {
            if (gv.Columns[i].SortExpression.Split(',')[0].Trim().ToLower() == columnName.ToLower())
            {
                result = i;
                break;
            }
        }

        if (result < 0)
        {            
            throw new Exception("No column in '" + gv.ID + "' has a SortExpression that starts with '" + columnName + "'");
        }

        return result;
    }
}

In this last version I've made some changes to the comparing of the SortExpression and the columnName parameter. By splitting the SortExpression on ',' (comma), trimming it and sending it to lower case I get a less error prone comparison that also will evaluate to true if the SortExpression starts with the columnName given. Also I decided to throw an exception if the columnName asked for is not found, so that such an error is easily found during development.

And finally, the final version of the RowDataBound event;

protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType == DataControlRowType.DataRow)
    {
        DataRowView dataItem = (DataRowView)e.Row.DataItem;
        if ((bool)dataItem["Discontinued"] == true)
        {
            int cellIndex = gvProducts.GetColumnIndex("ProductName");
            e.Row.Cells[cellIndex].ForeColor = Color.Red;
        }
    }
}

Instead of manipulating the ForeColor property of the TableCell you should consider assigning the CssClass property instead, and have a style sheet in place for the actual textformatting you want done. This way, when the color needs to be changed all you have to do is change the stylesheet instead of the C# code.

Googled links during the making of this blogpost

Switching to english

When I first started to write this blog I decided to do it in swedish mainly for two reasons;

  1. There aren't many blogs on the topic written in swedish.
  2. It will be a whole lot easier for me to get posts written.

The first one is still true, but the second one might not be. One problem with writing in swedish is that not that many will find the blog, since even the swedes will google for solutions to their programming problems in english. So for the time being I will switch to english to see if that in any way increases traffic to the blog, which might encourage me to write more often. Please feel free to comment and rate my articles (as well as my english ;-). Any feedback, good or bad, is most welcome.

Of course my posts in swedish will remain and even if you don't understand this northern language you might get something out of looking at the screen caps and code snippets.

Länk med querystring i XML

Idag har jag lärt mig att om du vill inkludera en URL i XML, där URL:en innehåller en querystring och därmed &-tecken så måste dessa skrivas om till &amp; för att det ska bli giltig XML. Till exempel så här;

<myXmlElement>
    Go to <a href="http://www.somepage.com?somekey=1&amp;anotherKey=2">the page</a>
</myXmlElement>

Om du sen har kod som läser XML-filen och till exempel skriver ut innehållet på en webbsida eller skickar med det i ett mejl så är det inga problem att &amp; hänger med även där, när man väl klickar på länken så att den hamnar i adressfältet i en webläsare så blir det ett helt vanligt &-tecken.

Kolla in fler Html Entities

I väntan på deap.nu

Inför att min nya personliga webbplats snart kommer ut på deap.nu har bloggen fått sig en ansiktslyftning och den nya adressen ondotnet.deap.nu. Så uppdatera dina bokmärken! :-)

Allt har jag inte gjort själv, så här kommer länkar till en del bra resurser jag använt till den här bloggen;

  • För att visa upp mina senaste programmeringsrelaterade bokmärken (t.v.) från delicious har jag använt deras linkroll-funktion, använd formuläret för att generera en liten javascript-referens, lägg in på bloggen och vips visas dina senaste länkar på din blogg/hemsida.
  • En liknande funktion har bloglines, den gör det möjligt att visa länkar till de bloggar jag prenumererar på. Fördelen är förstås att i samma stund som jag väljer att prenumerera på bloggen så visas länken även här utan att något extra steg behöver tas.
  • Till höger har jag använt detta script för att tweaka om bloggers label-lista till ett taggmoln, och för att rita molnet har jag kikat på denna photoshop-tutorial.

deap.nu kommer att bli en liten orgie i javascript eller rättare sagt jQuery som är ett fantastiskt javascript-ramverk som nyligen fått intellisense-stöd i visual studio(!). Kolla in jQuery.com.

Generera html-element på klientsidan

Har mixtrat lite med att generera html-element via javascript, här följer ett enkelt exempel:

<html><head></head><body>

<script type="text/javascript">

function Add()
{
var input = document.createElement('input');
input.setAttribute('type', 'text');

var addArea = document.getElementById('addArea');
addArea.appendChild(input);
}

</script>

<a href="javascript:Add();">Add</a>

<div id="addArea">
</div>

</body></html>

I javascript-funktionen Add skapas alltså ett html-input-element och attributet type sätts till text. (<input type="text" />) Sedan läggs textboxen till i diven med id addArea. Detta sker varje gång man klickar på Add-länken.

Min nästa fundering var om man på klientsidan kan associera denna scriptgenererade textbox med exempelvis Ajax Control Toolkits Masked Edit Extender. Så jag slängde ut en fråga om det på Asp.net Forums. Om det går kan det vara bra att veta...

Validering av checkboxlista

I ett skolprojekt som i detta nu håller på att bli färdigställt har jag gjort en enkät-funktionalitet där några av frågorna innebär att man kan kryssa i flera alternativ, men man måste kryssa i minst ett.

Checkboxar och Checkboxlistor går dock inte att validera med en vanlig RequiredFieldValidator eftersom en ej ikryssad checkbox faktiskt också är ett val. Så vi får ta till en CustomValidator och själva skriva kod både på serversidan och klientsidan (javascript) för att genomföra valideringen på ett säkert (serversidan) och användarvänligt (klientsidan) sätt.

Vi börjar med att fixa en fråga och en checkboxlista, så här ser aspx-koden ut:

Hur reser du till/från jobbet? (flera svar möjliga)<br />
<asp:CheckBoxList ID="cblWorkTravels" runat="server">
    <asp:ListItem>Åker bil</asp:ListItem>
    <asp:ListItem>Åker kommunalt</asp:ListItem>
    <asp:ListItem>Går</asp:ListItem>
    <asp:ListItem>Cyklar</asp:ListItem>
    <asp:ListItem>Annat</asp:ListItem>
</asp:CheckBoxList>

<asp:Button ID="btnReply" runat="server" Text="Svara" />

Vi kommer fokusera på valideringen här så vad som händer när man klickar på knappen är upp till dig, men validera kommer den att göra oavsett.

Nästa steg är att lägga till en CustomValidator och implementera dess ServerValidate-event. Metodsignaturen för det eventet ser ut så här;

protected void cvWorkTravels_ServerValidate(object source, ServerValidateEventArgs args)

Det andra argumentet, args, har en boolean property vid namn IsValid som är av särskilt intresse. Uppgiften är nu att skriva kod i metoden som sätter IsValid till true om valideringen ska anses lyckas, och annars till false;

protected void cvWorkTravels_ServerValidate(object source, ServerValidateEventArgs args)
{
    bool result = false;
    foreach (ListItem item in cblWorkTravels.Items)
    {
        if (item.Selected)
        {
            result = true;
            break;
        }
    }
    args.IsValid = result;
}

Så vi loopar igenom alla alternativ i checkboxlistan och så fort vi hittar ett alternativ som är markerat så avbryter vi loopen och sätter args.IsValid till true. Om inget alternativ är valt kommer args.IsValid att sättas till false.

Nu går det utmärkt att testa detta men den röda lilla stjärnan som indikerar att man gjort fel kommer att visas först efter en postback. Så kan vi inte ha det, vi måste förstås fixa ett javascript som validerar detta i klienten, särskilt om du kombinerar en CustomValidator med andra valideringskontroller som så fint fixar valideringen även på klientsidan.

Vår CustomValidator har en property vid namn ClientValidationFunction som ska innehålla namnet på den javascriptfunktion som gör valideringen. Vi sätter den till "validateWorkTravels" och infogar följande scriptblock i aspx-sidan;

function validateWorkTravels(sender, args)
{
    var result = false;
    for(var i = 0; i < <%=cblWorkTravels.Items.Count %>; i++)
    {
        var checkBox = $get('<%=cblWorkTravels.ClientID %>_' + i);
        if(checkBox.checked)
        {
            result = true;
            break;
        }
    }
    args.IsValid = result;
}

Logiken här är exakt densamma som på serversidan, men intressant att notera här är hur jag kommer åt själva checkboxelementen i javascript-koden.

Till att börja med måste jag säkerställa att jag inte loopar fler gånger än vad det finns checkboxar, det gör jag genom att hoppa över till serversidan och hämta ut antalet precis som om jag hade gjort det i C#-koden. Observera att du inte får någon intellisense från Visual Studio här, så var noga med stavning och små/stora bokstäver. Du kommer också få nån markering på serverscriptavgränsaren <%= men det är bara att ignorera.

Två rader ned accessar jag en checkbox via Asp.NET AJAX-genvägen $get (en ScriptManager har jag alltså lagt till i sidan) och där hämtar jag ut ClientID för checkboxlistan och lägger till _0, _1 etc för det är den namnkonvention som Asp.NET använder när den sätter ID på checkboxarna. De tre fetstilta raderna ovan renderas alltså ut till webbläsaren så här;

for(var i = 0; i < 5; i++)
{
    var checkBox = $get('cblWorkTravels_' + i);

Det fetstilta i detta exempel är det som kommer från <%= %>-avdelningarna i kodexemplet längre upp.

I det skarpa exemplet var det förstås så att det förekom flera frågor som fungerade på samma sätt. Det föranledde följande omarbetning av koden på serversidan;

protected void cvWorkTravels_ServerValidate(object source, ServerValidateEventArgs args)
{
    args.IsValid = IsAnyChecked(cblWorkTravels);
}

private bool IsAnyChecked(CheckBoxList cbl)
{
    bool result = false;
    foreach (ListItem item in cbl.Items)
    {
        if (item.Selected)
        {
            result = true;
            break;
        }
    }
    return result;
}

Nu kan vi lägga till fler Checkboxlistor som har varsitt ServerValidate-event som i sin tur anropar metoden IsAnyChecked. Här följer hela den slutgiltiga aspx-koden med samma förändring i javascriptdelen;

<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server" />
    <div>    
        Hur reser du till/från jobbet? (flera svar möjliga)
        <asp:CustomValidator ID="cvWorkTravels" runat="server" 
            Text="*" onservervalidate="cvWorkTravels_ServerValidate" 
            ClientValidationFunction="validateWorkTravels" />
        <br />
        <asp:CheckBoxList ID="cblWorkTravels" runat="server">
            <asp:ListItem>Åker bil</asp:ListItem>
            <asp:ListItem>Åker kommunalt</asp:ListItem>
            <asp:ListItem>Går</asp:ListItem>
            <asp:ListItem>Cyklar</asp:ListItem>
            <asp:ListItem>Annat</asp:ListItem>
        </asp:CheckBoxList>
        
        <asp:Button ID="btnReply" runat="server" Text="Svara" />
    </div>
    </form>
    <script type="text/javascript">
        //<![CDATA[

        function validateWorkTravels(sender, args)
        {
            args.IsValid = IsAnyChecked('<%=cblWorkTravels.ClientID %>', <%=cblWorkTravels.Items.Count %>);
        }
        
        function IsAnyChecked(cblClientID, cblLength)
        {
            var result = false;
            for(var i = 0; i < cblLength; i++)
            {
                var checkBox = $get(cblClientID + '_' + i);
                if(checkBox.checked)
                {
                    result = true;
                    break;
                }
            }
            return result;            
        }
            
        //]]>
    </script>  
</body>

En skillnad i javascriptet blir att metoden IsAnyChecked får ta emot både ID:t för checkboxlistan, samt dess längd eftersom det är något jag inte kan få ut på klientsidan.

Finally körs verkligen ALLTID

Häromveckan gick vi igenom try/catch i skolan och det dök upp en del frågetecken kring vad finally-blocket ska vara till för. Skriver man kod efter try/catch-blocket så körs ju den. Men vad händer om man har kod i catch-blocket som till exempel skickar vidare användaren till en annan sida (Asp.NET-applikation)?

Jag bestämde mig för att testa detta. Först skapade jag en ny website-applikation och skrev in följande kod i Page_Load för startsidan Default.aspx;

Session["Message"] = "Everything is working";

try
{
    //trigga ett DivideByZeroException
    int divider = 0;
    int i = 3 / divider;
}
catch
{
    Response.Redirect("default2.aspx");
}
finally
{
    Session["Message"] = "Something went wrong";
}

Sedan la jag till en ny aspx, default2.aspx som helt enkelt skriver ut värdet på Session["Message"] till en label;

protected void Page_Load(object sender, EventArgs e)
{
    lblMessage.Text = Session["Message"].ToString();
}

Och resultatet är precis som min rubrik antyder, att finally-blocket körs innan Response.Redirect i catch-blocket skickar vidare till default2.aspx och resultatet blir att "Something went wrong" skrivs ut på sidan. Stegar man med debuggern körs dock faktiskt Response.Redirect-raden innan stegningen går vidare till finally, så det hela är minst sagt automagiskt... ;-)

Att fundera på

Session["Message"] = "BeforeTryCatch";

try
{
    Session["Message"] += " EnteringTry";
    //trigga ett DivideByZeroException
    int divider = 1;
    int i = 3 / divider;
    Session["Message"] += " LeavingTry";
}
catch
{
    Session["Message"] += " EnteringCatch";
    Response.Redirect("default2.aspx");
    Session["Message"] += " LeavingCatch";
}
finally
{
    Session["Message"] += " InFinally";
}

Session["Message"] += " AfterTryCatch";

Se koden ovan
1. Vad kommer Session["Message"] innehålla om något går fel i try-blocket?
2. Vad kommer Session["Message"] innehålla om inget går fel i try-blocket?

Markera här för rätt svar:
1. "BeforeTryCatch EnteringTry EnteringCatch InFinally". Alltså, raderna där "LeavingTry", "LeavingCatch" och "AfterTryCatch" står kommer aldrig att nås om något går fel i try-blocket eftersom Response.Redirect skickar vidare. Med finally-blocket får du en möjlighet att göra något innan användaren skickas vidare.
2. "BeforeTryCatch EnteringTry LeavingTry InFinally AfterTryCatch". Om inget går fel går allt utom catch-blocket igenom.
==================================================================================

Lektionsbloggen: Design Patterns

Idag snackade vi Design Patterns, googlade fram några länkar på de patterns vi gick igenom:

Singleton Pattern
FlyWeight Pattern
Method Template Pattern

Vi pratade även om State Design Pattern, men hittar ingen bra sida om den lite snabbt just nu.

Obligatorisk läsning om ViewState

Det här är obligatorisk läsning för samtliga Asp.NET-utvecklare. Bloggposten är inte så läsvänlig på skärm men det går utmärkt att markera texten och skriva ut markeringen. Läs allt men fokusera kanske lite mer på de konkreta tipsen i slutet av artikeln.

Lektionsbloggen: Gratis E-learning från Microsoft och InnerWorkings

Hela denna vecka handlar utbildningen om WPF - Windows Presentation Foundation. Istället för att blogga om detta tänkte jag tipsa om att Microsoft E-learning erbjuder en gratis elearning-introduktion om WPF på www.microsoftelearning.com. Så här hittar du den:

  • Logga in med ditt Windows Live ID.
  • Klicka på "My Learning" i vänstermenyn".
  • Du kommer nu till en sida med lite annorlunda layout än tidigare. I högerspalten listas "Gratisprodukter", efter några poster finns länken "Visa alla...". Klicka på den.
  • Scrolla ner till "Collection 6261: Developing Rich Experiences using Microsoft .NET Framework 3.5 & Visual Studio 2008". Välj den.
  • På nästa sida listas tre kurser ("Clinics"). Den i mitten handlar om WPF. Klicka på den.
  • Kryssa i kryssrutan uppe till höger och klicka på knappen "Aktivera kostnadsfritt innehåll". Observera att du kommer ha tillgång till kursen i max ett år från att du aktiverar den.
  • Nu kommer du till kursens sida och du kan köra igång.

Microsoft Elearning-kurserna är lite småsega och det är ganska mycket att läsa. Men jag tycker ändå de är ett bra avbrott till att läsa i boken och de innehåller bra exempel och ganska roliga labbar (även om de också är lite sega, då man måste ansluta via webbläsaren till en labbdator nånstans hos Microsoft). Igår hade jag dock problem med diverse felmeddelanden som dök upp, men jag hoppas att det ska vara löst till nu. Kolla även in de andra gratiskurserna som finns här, t.ex. finns det en om Asp.NET AJAX.

En betydligt bättre labbmiljö erbjuder utbildningsföretaget InnerWorkings, nämligen din egen dator med Visual Studio etc. Även de erbjuder ett antal gratiskurser, bland annat om C# och Linq. Om du går till www.innerworkings.com så hittar du gratiskurserna så här:

  • Klicka på "Trial" i huvudmenyn till höger.
  • Fyll i formuläret för att skapa dig ett konto och välj sedan en av de gratis trial-kurserna. (Det kan verka som att man bara får välja en, men du kan återvända till denna sida senare, välja länken "simply sign-in" och välja ytterligare en av kurserna.)
  • Följ sedan instruktionerna. Du kommer att få ladda hem ett program som körs lokalt på din dator där kursen verkar. Dessutom installeras en Visual Studio Add-On som gör att du under labbarna har tillgång till ett fönster inne i Visual Studio med instruktioner(!).
  • Dessutom kan du logga in via www.innerworkings.com/promotions för ytterligare ett antal gratiskurser om Linq, WPF och AJAX. Du kommer först till WPF-sidan (just nu), välj bland de andra i rutan "Current Promotions". När du valt, klicka på den orange "Register Now"-rutan. Här kan du återigen välja "Simply sign-in" om du redan skapat ditt konto och därefter ladda ner kursen. Den här sidan lär det säker finnas anledning att återkomma till då och då för att se om de erbjuder nya kurser gratis. Jag kollade vad en kurs kostar om man vill betala, kommer inte ihåg vilken nu, och priset var $29.99 vilket jag tyckte var lite väl saftigt...

Jag gjorde Linq-kursen igår och det var verkligen kul. Jättebra småövningar att komplettera utbildningen med och kul labbmiljö där man får ett påbörjat projekt med ett avgränsat antal metoder som ska implementeras. När man är klar kan man dels få koden "rättad" av en "code checking enginge" och dessutom kan man öppna upp en facit-version av hela lösningen för att se hur de har tänkt sig att man ska skriva koden (den automatiska rättningen ger rätt även om man inte gjort exakt som facit-lösningen så det kan verkligen vara värt att kika på den!).

Lektionsbloggen: Linq to Sql

Lektionens datum: 6/8 2008 
Lektionens rubrik: Linq (2/2)
Lektionsbloggens fokus: Linq to Sql och Lambda-expressions

Idag handlade lektionen om Linq to Sql och Lambda-expressions.

Förutsättningar
Innan vi kan sätta igång måste vi få en databas på plats. Mina exempel kommer använda sig av exempeldatabasen Northwind. Ladda hem Northwind, packa upp zip-filen på en plats där du vet var du har den. Observera att detta exempel kommer att kräva att du har Sql Server eller Sql Server Express.

Skapa ett nytt windows-projekt. Välj sedan Add -> Existing Item..., bläddra fram till och välj NORTHWND.mdf. Nu kommer fönstret Data Source Configuration Wizard att öppnas, ignorera det genom att välja Finish. Svara "Ja" på varningen som kommer upp. Nu syns följande i Solution Explorer;
image

Högerklicka på .xsd-filen och välj att ta bort denna. Vi ska nämligen använda Linq to Sql och inte ett dataset (bloggen lär återkomma till detta någon annan gång).

Linq to Sql
Nu när vi har databasen på plats ska vi ordna så att Visual Studio genererar klasser för de tabeller i databasen som vi vill jobba med. Välj Add -> New Item... och välj mallen LINQ to SQL Classes;
image

Döp filen till Northwind.dbml.

När du klickat Add kommer du se den nya noden Northwind.dbml i Solution Explorer. Under den finns filerna Northwind.cs, Northwind.dbml.layout och Northwind.designer.cs.

I Visual Studios huvudfönster är Northwind.dbml öppet och ett fönster som är delat i två vertikala avdelningar visas. Till den vänstra kan du dra in tabeller från databasen och i den högra kan man dra in stored procedures. För varje objekt som dras in genereras kod som hamnar i Northwind.designer.cs. Dra in tabellerna Customers och Orders i Northwind.dbml;
image
Dra tabeller från Server Explorer till Northwind.dbml:s vänstra halva.

Gränssnitt i Form1
Nu ska vi göra ett enkelt gränssnitt i Form1.cs. Öppna designläget, gör fönstret lite större och dra ut en DataGridView som du ger namnet gvOutput samt en knapp med namnet btnShowCustomers och texten "Visa alla kunder";
image

Linq to Sql-fråga
Dubbelklicka på knappen och skriv kod för att ta fram alla kunder;

private void btnShowCustomers_Click(object sender, EventArgs e)
{
    NorthwindDataContext db = new NorthwindDataContext();

    var output = from customer in db.Customers
                 select customer;

    gvOutput.DataSource = output;
}

Först skapar vi en referens, db, av typen NorthwindDataContext. Detta är en typ som finns deklararerad i Northwind.designer.cs och den krävs för att vi ska kunna accessa de tabeller som vi valt att inkludera i .dbml-filen. Därefter kan vi skriva en enkel Linq-fråga och koppla resultatet till DataGridView-kontrollens DataSource-property.

I just det här fallet är Linq-syntaxen helt onödig. Eftersom vi vill visa alla kunder hade vi lika gärna kunnat skriva så här, eftersom db.Customers är en samling av Customer-objekt.

gvOutput.DataSource = db.Customers;

Men säg att vi vill visa endast kunder från Sverige istället. Lägg in en ny knapp, döp den till btnSwedishCustomers och ge den texten "Visa alla kunder från Sverige". Flytta ut deklarationen av NorthwindDataContext-objektet db direkt i klassen Form1 (så att vi har tillgång till den i alla metoder). Implementera sedan följande kod i knappens Click-event;

private void btnSwedishCustomers_Click(object sender, EventArgs e)
{
    var output = from customer in db.Customers
                 where customer.Country == "Sweden"
                 select customer;
    gvOutput.DataSource = output;
}

Här villkorar vi alltså resultatet genom att kräva att customer.Country=="Sweden".

Lambda-variant
Exakt samma filtrering kan också göras med hjälp av mer klassiska metodanrop samt lambda-uttryck;

var output = db.Customers.Where(customer => customer.Country == "Sweden");

Här anropas metoden Where direkt på db.Customers. Where är egentligen en metod som tar emot en delegatparameter. Denna delegatparameter kräver ett customerobjekt som parameter (eftersom vi anropar den på en instans av en Customer-samling) och returnerar bool. Men med hjälp av lambda-operatorn => kan vi direkt deklarera inputparametern (före operatorn) och sedan kod som kan utvärderas till true eller false (efter operatorn). customer (med litet c!) är alltså vår identifierare/input-parameter. I Where-metoden kommer den i en loop att anta varje Customer-objekt som finns i db.Customers. Om objektets Country-property har värdet "Sweden" kommer det objektet att inkluderas i resultatet, annars inte.

Låt användaren filtrera ordrar per kund
Vi ska nu göra ett mer avancerat/dynamiskt exempel där användaren får välja bland alla kunder och när han valt en kund visas en lista över kundens ordrar. Börja med att dra in en ComboBox i fönstret och namnge den ddlCustomers. Implementera sedan följande kod i Form1-klassens constructor (så att ComboBoxen fylls direkt när fönstret öppnas);

foreach (Customer c in db.Customers)
{
    ddlCustomers.Items.Add(c);
}

Om du nu kör programmet kommer du få en dropdownlista där samtliga val visar exakt samma text, nämligen "applikationensnamn.Customer". Detta beror förstås på att Items i en ComboBox är av typen Object och när ComboBoxen visar upp sitt innehåll förlitar den sig på att den faktiska typen har en ToString-metod som ger objektet en vettig strängrepresentation. Om en sådan inte finns körs Object-klassens version som kort och gott skriver ut namnet på objektets typ. För att fixa detta öppnade jag filen Nortwhind.designer.cs, letade mig fram till deklarationen av klassen Customer och la till följande implementation av ToString;

public override string ToString()
{
    return string.Format("{0}: {1}", CustomerID, ContactName);
}

Om du studerar klassen Customer kommer du se att den har publika properties som heter CustomerID och ContactName (bland flera andra) och dessa properties get- och set-block tar hand om hanteringen för hur värdena hämtas från databasens tabellceller. Jag åker snålskjuts på dessa och får således en vettig strängrepresentation för varje Customer.

Låt oss nu titta på den kod som ska implementeras i ComboBoxens SelectedIndexChanged-event;

private void ddlCustomers_SelectedIndexChanged(object sender, EventArgs e)
{
    Customer selectedCustomer = ddlCustomers.SelectedItem as Customer;
    var output = from order in db.Orders
                 where order.Customer == selectedCustomer
                 select order;
    gvOutput.DataSource = output;
}

Vi börjar med att typa om ComboBoxens SelectedItem (som är av typen Object) till en Customer och placerar den i referensen selectedCustomer. Därefter kan vi använda den referensen i vårt Linq-uttryck för att filtrera ut just de ordrar som hör till denna kund.

image

Jag stannar här för idag. Lektionen tog även upp hur vi kan spara in och ta bort poster i databasen, t.ex. genom att skapa ett nytt object av typen Customer, och sedan på NortwhindDataContext-objektet db anropa db.Customers.InsertOnSubmit och därefter db.SubmitChanges().

Imorgon blir det lite Linq to XML, samt serialisering.

Ladda hem "the Lektionsbloggen Solution"
Tips! I den nedladdningsbara versionen finns en del kommentarer som stöd när du läser koden.

Lektionsbloggen: LINQ och ny C#-syntax

Lektionens datum: 5/8 2008
Lektionens rubrik: Linq (1/2)
Lektionsbloggens fokus: LINQ to Objects och diverse ny C#-syntax

Dagens lektion introducerade LINQ och demade också ett antal nya syntax-möjligheter i C#.

Automatiska properties
Här följer två varianter på exakt samma klass. I exemplet till höger används C# 2008:s nya sätt att skriva en property som automatiskt ger en privat datamedlem som kapslas in av den publika propertien;

Traditionellt sätt:
class Contact
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }

    private string _city;
    public string City
    {
        get { return _city; }
        set { _city = value; }
    }
}
Ny C#-syntax:
class Contact
{
    public string Name { get; set; }
    public string City { get; set; }        
}

Som du ser kan man spara in en hel del kod på det här sättet. Observera dock att det endast är möjligt att använda denna kortvariant om du inte har behov av att hantera propertien på något sätt i get- eller set-blocket.

Object Initialization Syntax
En annan syntax-nyhet i C# 2008 är Object Initialization. Tidigare var man tvungen att initera ett objekts properties med värden antingen genom en constructor eller så här:

Contact a = new Contact();
a.Name = "Ola Salo";
a.City = "Malmö";

Med den nya syntaxen kan du skriva exakt samma sak så här:

Contact b = new Contact { Name = "Alexander Bard", City = "Stockholm" };

Återigen, du sparar in en del tangenttryckningar och dessutom minskar behovet av att förse klassen med en constructor.

LINQ-introduktion
LINQ gör det möjligt att på ett enhetligt sätt hantera data från olika datakällor. En datakälla kan exempelvis vara en SQL-databas, ett XML-dokument eller rent av en lista med objekt. Se följande kod, där vi återanvänder objekten a och b från ovan;

List<Contact> contactList = new List<Contact>();
contactList.Add(a);
contactList.Add(b);
contactList.Add(new Contact { Name = "Lasse Kronér", City = "Göteborg" });
contactList.Add(new Contact { Name = "Nina Persson", City = "Jönköping" });
contactList.Add(new Contact { Name = "Tina Nordström", City = "Helsingborg" });
contactList.Add(new Contact { Name = "Pekka Heino", City = "Stockholm" });

var sthlmCitizens = from contact in contactList where contact.City=="Stockholm" select contact;

foreach (Contact c in sthlmCitizens)
{
    Console.WriteLine(c.Name);
}

Själva linkuttrycket är fetstilt ovan och följer följande struktur:
from identifier in list where condition select identifier

Notera att sthlmCitizens-referensen är deklarerad med nyckelordet var. var är ett nyckelord som gör det möjligt att deklarera en variabel och sedan låta första tilldelningen till variabeln avgöra vilken datatyp variabeln är. I detta fall returernar Linq-uttrycket ett objekt av typen IEnumerabe<Contact>, men det är inte alltid man kan veta, så var är en smidig genväg. Det du kan vara säker på är att ett Linq-uttryck alltid returnerar ett objekt som går att loopa med foreach.

När Linq-uttrycket körs loopar den igenom listan (contactList) för att se vilka objekt i listan (kan även vara en collection, array etc.) som uppfyller villkoret (contact.city=="Stockholm"). Identifier är den variabel som för varje loop innehåller det aktuella objektet, precis som c i foreach-loopen. Linq-uttrycket avslutas med en select-sats där man talar om vad som ska väljas ut, i det här fallet hela contact-objektet vilket innebär att jag i foreach-loopen kan typa c som en Contact och komma åt både c.Name och c.City. Man kan begränsa detta genom att förändra select-satsen i Linq-uttrycket enligt följande;

var sthlmCitizensNames = from contact in contactList where contact.City == "Stockholm" select new { contact.Name };

Det som händer här är att en helt ny datatyp skapas "on-the-fly" via new. Denna datatyp innehåller endast en property, nämligen Name. Eftersom vi inte kan veta namnet på denna datatyp innebär det att vi nu måste använda var i den foreach-loop som skriver ut träffarna;

foreach (var c in sthlmCitizensNames)
{
    Console.WriteLine(c.Name); //c.City finns inte här
}

Man kan också låta Linq-uttrycket sortera objekten, t.ex. så här;

var sthlmCitizensOrdered = from contact in contactList where contact.City == "Stockholm" orderby contact.Name descending select contact;

Apropå mina tidigare inlägg om sortering så är ju det här ganska intressant med tanke på att klassen Contact inte implementerar IComparable och vi har inte gjort någon separat IComparer-klass heller. Nyckelordet descending kan utelämnas eller ersättas med ascending, man kan också andrahandssortera genom att kommaseparera de properties man vill sortera på.

Extension Methods
En annan nyhet i C# 2008 är möjligheten att utöka redan existerande klasser med egna metoder. I följande exempel kommer jag att utöka klassen char med en metod som returnar true om tecknet är en vokal och annars false. För att detta ska vara möjligt måste metoden placeras i en static klass, metoden i sig måste vara public och static och metodens första parameter måste vara av den typ som jag vill utöka samt föregås av nyckelordet this. Dessa förutsättningar kommer i fetstilt här;

public static class CharExtensions
{
    public static bool IsVowel(this char letter)
    {
        letter = Convert.ToChar(letter.ToString().ToLower());

        char[] vowels = { 'a', 'o', 'u', 'å', 'e', 'i', 'y', 'ä', 'ö' };        
        return vowels.Contains<char>(letter);
    }
}

För att metoden ska hantera både versaler och gemener korrekt börjar jag med att i metodens första rad konvertera bokstaven till en sträng och göra den till liten bokstav via String-klassens ToLower-metod. Därefter konverteras tecknet tillbaka till en char via Convert.ToChar.

Sen deklarerar jag en array av char som innehåller alla vokaler (som gemener) och slutligen anropar jag char-arrayens Contains-metod som kommer att returnera true när letter är en vokal, annars false.

Nu kan denna metod anropas direkt på en instans av en char;
image

I IntellieSense-fönstret ser du att det är en extension-method på den blå lilla pilen i ikonen, samt att tooltipfönstret markeras med "(extension)".

Imorgon ska lektionen handla om Linq to SQL.

Ladda hem "the Lektionsbloggen Solution"

Ta kontroll över dina solutions och projects

En lite knepig grej är hur Visual Studio väljer att organisera filer när man gör nya projekt. Säg att du har gjort en folder som heter DotNetProjects där du vill lagra alla dina olika programmeringsprojekt. Sedan skapar du ett nytt Web Site-project enligt följande;
image
 

image

Nu skapas självklart en mapp under DotNetProjects som heter MyWebsite. Men dessutom skapas följande katalog under Mina Dokument;
image

Här placeras en .sln-fil som symboliserar din solution. En solution kan innehålla flera WebSites, WinForm-program, Class Libraries etc. Mer om det strax. Dessutom finns här en .suo-fil som håller koll på Visual Studio-inställningar för just denna Solution (och programmerare).

En viktig poäng här är att dessa filer inte på något sätt behövs för att du ska kunna jobba med ditt WebSite-projekt. Du kan välja File -> Open Website... och peka ut C:\DotNetProjects\MyWebsite för att öppna upp och jobba med WebSite-projektet. Om du tar bort .sln- och .suo-filerna kommer Visual Studio skapa nya nästa gång du öppnar och sparar projektet.

.sln-filen är dock bra att ha koll på. Bland annat eftersom det är den Visual Studio länkar till under Recent ProjectsStartPage-fliken;
image

Dessutom håller .sln-filen koll på vilka filer du hade öppna senast du jobbade med projektet, vilket kan vara smidigt när man ska fortsätta där man senast slutade.

Så för att få en bättre koppling även i filsystemet mellan .sln-filen och själva projektet brukar jag göra följande:

  1. Skapa en Blank Solution
  2. Lägga till projekt, som jag placerar under den katalognivå där jag placerat .sln-filen.

Innan vi kikar på detta gör jag om min grundläggande katalogstruktur enligt följande;
image

Som du ser har jag lagt till en Rot-nivå, flyttat in DotNetProjects och döpt om den till DotNetSolutions för att bättre stämma överens med Visual Studios terminologi. Anledningen till denna rotnivå återkommer jag till.

Nu skapar jag en Blank Solution enligt följande:
image

image

Notera att jag i vänsterkolumnen navigerat mig till Other Project Types -> Visual Studio Solutions för att hitta mallen Blank Solution. Notera också att jag med Browse...-knappen pekat ut min DotNetSolutions-mapp.

Nu skapas ingenting annat än en helt tom solution, Solution Explorer ser ut så här:
image

Och i Utforskaren ser det ut så här:
image

Nu vill jag ordna så att MyWebsite blir ett WebSite-projekt som hör till MyWellOrganisedSolution. Jag börjar med att flytta in mappen MyWebsite i mappen MyWellOrganisedSolution;
image

Sedan går jag tillbaka till Visual Studio och gör enligt följande;
image

image

Redan vid mappförflyttningen ovan åstadkom vi att filsystemet speglar att MyWebsite är en del av MyWellOrganisedSolution. Genom detta sista steg är det så även enligt Visual Studio och .sln-filen;
image

Nu kan vi också städa bort den uppsättning .sln/.suo-filer Visual Studio skapade när vi först skapade MyWebsite;
image
(Testa gärna att dubbelklicka på denna MyWebsite.sln innan du tar bort mappen, Visual Studio kommer inte att hitta WebSite-projektet MyWebsite då det har flyttats sedan vi skapade det)

Ett inte helt ovanligt scenario är att man har projekt som ska ingå som en del i flera andra projekt. Det kan vara projekt man gör själv, eller något delat projekt som exempelvis AjaxControlToolkit. "Projekt?" kanske du tänker nu, ja men visst består AjaxControlToolkit av projekt, låt oss kika på katalogstrukturen för AjaxControlToolkit (Ladda hem Ajax Control Toolkit, välj AjaxControlToolkit-Framework3.5.zip);
image

Om du kikar i mappen AjaxControlToolkit kommer du hitta en fil som heter AjaxControlToolkit.csproj. Detta är ett Visual Studio-projekt som är ett Class Library, varje mapp innehåller klasser och andra filer som utgör en kontroll i toolkitet. Det finns dessutom en bin-mapp där toolkitets dll-fil ligger. Detta är den kompilerade versionen av toolkitet, som andra projekt behöver för att kunna använda sig av kontrollerna i toolkitet.

Om du kikar i mappen SampleWebSite så kommer du se att den innehåller mappar som App_Code, App_Data och filer som Default.aspx och web.config. Denna mapp går alltså att öppna via File -> Open -> Web Site... (Eller så kan du se AjaxControlToolkit här, det är exakt samma website).

Om du kikar direkt i rotmappen, AjaxControlToolkit-Framework3-5, kommer du hitta en .sln-fil som kapslar in båda dessa projekt och några till som ingår när man laddar hem AjaxControlToolkit. Ok, detta var ett litet sidospår samtidigt som det var repetition ;-)

Det som kan vara intressant för våran MyWebsite är förstas klassbiblioteket AjaxControlToolkit. Men detta projekt är ju antagligen intressant att också inkludera i andra solutions man gör, så därför känns det fel att kopiera denna mapp in i mappen MyWellOrganisedSolution. Därav rotnivån jag skapade för mina DotNetSolutions tidigare, jag skapar nu en ny mapp kallad Library och där i placerar jag hela AjaxControlToolkit enligt följande;
image

Tänk på mappen Library som ett bibliotek av projekt som kommer att användas av mer än en av dina solutions.

Nu kan jag lägga till AjaxControlToolkit till MyWellOrganisedSolution så här;
image

image
Markera AjaxControlToolkit.csproj och klicka Öppna. En dialogruta visas där du får välja mellan Load project for browsing/Load project normally. Du måste välja det senare om du har behov av att ändra i det projekt du lägger till och i princip kan du alltid göra det så länge källan till projektet är tillförlitlig.

Nu finns båda projekten i Solution Explorer;
image

Du kan nu öppna upp filerna under AjaxControlToolkit och om du vet vad du gör kan du ändra på befintliga kontroller och kanske rentav skapa egna(!). Och om du gör det och lägger till AjaxControlToolkit på liknande sätt i en annan WebSite-solution kommer det vara samma filer du redigerar i oavsett vilken solution du just då har öppen.

För att du ska kunna använda AjaxControlToolkit i MyWebsite måste dock ytterligare en sak göras. Högerklicka på MyWebsite och välj Add Reference...;
image

I dialogrutan som öppnas, välj fliken Projects;
image

Här visas en lista över alla övriga projekt i aktuell solution (just nu finns det ju bara ett) och du kan alltså enkelt välja AjaxControlToolkit. Det som händer när du gör det är att det projektets dll-fil kopieras till MyWebsite-projektets bin-mapp;
image

Om du ändrar något i AjaxControlToolkit (tänk på att detta också gäller om du lagt till ett eget klassbibliotek) måste du högerklicka på solution-noden och välja Rebuild Solution innan ändringarna slår igenom i MyWebsite.

Låt oss ta en snabb koll på ett eget Class Library också, i detta fall som ett projekt som bara kommer att användas av MyWebsite. Att göra ett separat Class Library istället för att placera kod-filer i App_Code gör bland annat att kompilering går snabbare, läs mer om det här.

Så, vi lägger till ett nytt projekt:
image

image

Notera att jag väljer att placera detta projekt under MyWellOrganisedSolution i filsystemet, eftersom detta projekt endast kommer att användas av MyWebsite, som man kan säga är huvudprojektet i denna Solution.

Min solution har nu följande struktur;
image

Lägg till en referens till MyClassLibraryForMyWebsite i MyWebsite, precis som du gjorde med AjaxControlToolkit. Därefter kan du lägga till x antal cs-filer i MyClassLibraryForMyWebsite och de kan användas av MyWebsite på precis samma sätt som om de hade legat i App_Code. Och som sagt, läs om skillnaden här.

Generell metod för att köra stored procedure

Blev lite trött på att det är mycket upprepning av kod när jag gör anrop till databas, det här är en typisk metod i en static-class (dbSelect, anropet blir alltså dbSelect.RoleId(/.../)) jag gjort vars metoder hämtar olika saker från databasen:

public static string RoleId(string roleName)
{
    SqlConnection conn = DbUtils.getConnection();
    SqlCommand sqlCom = new SqlCommand("getRoleId", conn);
    sqlCom.CommandType = CommandType.StoredProcedure;
    sqlCom.Parameters.AddWithValue("@roleName", roleName);
    
    conn.Open();
    object returnValue = sqlCom.ExecuteScalar();
    conn.Close();
    
    return returnValue.ToString();
}

Jag har flera metoder som ser i princip likadana ut, enda skillnaden är returtyp, namn på stored procedure, antal och vilka parametrar som krävs samt vilken typ av exekvering som ska göras på SqlCommand-objektet (fetstilt ovan). Så jag gjorde en generell metod enligt följande:

public enum ExecutionType
{
    Scalar,
    Reader,
    NonQuery
}

public static object RunStoredProcedure(string storedProcedure, ExecutionType executionType, params SqlParameter[] parameters)
{
    SqlConnection conn = DbUtils.getConnection();
    SqlCommand sqlCom = new SqlCommand(storedProcedure, conn);
    sqlCom.CommandType = CommandType.StoredProcedure;

    foreach (SqlParameter parameter in parameters)
    {
        sqlCom.Parameters.Add(parameter);
    }

    conn.Open();
    object returnValue = null;

    switch (executionType)
    {
        case ExecutionType.Scalar:
            returnValue = sqlCom.ExecuteScalar();
            conn.Close();
            break;
        case ExecutionType.NonQuery:
            sqlCom.ExecuteNonQuery();
            conn.Close();
            break;
        case ExecutionType.Reader:
            returnValue = sqlCom.ExecuteReader(CommandBehavior.CloseConnection);
            break;
    }

    return returnValue;
}

Till den här metoden kan jag alltså skicka in namnet på den stored procedure jag vill köra, vilken typ av exekvering via den egna enumeratorn ExecutionType samt valfritt antal (tack vare params-nyckelordet) SqlParameter-objekt. Metoden returnerar ett Object som jag får typa om till det som förväntas av den Stored Procedure jag anropar. Alla metoder i dbSelect-klassen har specifik returtypning så när metoderna väl anropas finns det inga generella Object-objekt att hantera.

Så metoderna i min dbSelect-klass kan nu slimmas ner till detta:

public static string RoleId(string roleName)
{
    object returnValue = DbUtils.RunStoredProcedure("getRoleId", ExecutionType.Scalar, new SqlParameter("@roleName", roleName));
    return returnValue.ToString();
}

Angående varianten då SqlCom.ExecuteReader(...) körs så skickar jag in parametern CommandBehavior.CloseConnection och conn-objektet stängs inte. Detta måste jag göra eftersom jag kommer vilja returnera hela SqlDataReader-objektet till den plats där metoden anropas och där hantera den data som finns i SqlDataReader-objektet. För att det ska gå måste conn-objektet vara öppet. Samtidigt måste jag ha möjlighet att stänga conn-objektet när jag är klar med SqlDataReadern och genom att skicka in CommandBehavior.CloseConnection när reader-objektet skapas så kommer conn-objektet automatiskt att stängas när jag anropar Close() på reader-objektet. Tänk dig att jag har en metod som returnerar en SqlDataReader med alla användare, den anropas så här och stängs när jag är färdig med den:

SqlDataReader reader = dbSelect.Users();
while(reader.Read())
{
    //Hantera varje post
}
reader.Close(); //Här stängs både reader-objektet och det conn-objekt
                //som skapades och öppnades i metoden RunStoredProcedure.
Angående CommandBehavior.CloseConnection, se detta .Net Tip of The Day.

Mera sortering

Efter lektionen i onsdags har jag funderat på varför det finns så många olika sätt att åstadkomma sortering (i detta blogginlägg kommer jag visa ett tredje sätt). Och i morse när jag vaknade alldeles för tidigt och inte kunda somna om så slog det mig.

Tänk dig att du nån gång måste sortera objekt av en klass som du inte själv har skrivit och som du inte har tillgång att ändra i. Och programmeraren som gjort klassen har inte implementerat interface:t IComparable (eller den implementation av IComparable som finns, sorterar inte objekten på det sätt som du vill sortera).

Då går det ju bra att göra en helt fristående klass som implementerar IComparer (Se även tidigare inlägg). Kolla in följande Console Application:

using System;
using System.Collections.Generic;
using System.Text;

// //////////////////////////////////////////
//Tänk dig att denna klass INTE går att ändra
public class Cat
{
public string Name { get; set; }
public int Age { get; set; }

public Cat(string name, int age)
{
Name = name;
Age = age;
}

public override string ToString()
{
return string.Format("Katt: {0}, {1}", Name, Age);
}
}
// //////////////////////////////////////////

class Program
{
public static List<Cat> catList = new List<Cat>();

static void Main(string[] args)
{
catList.Add(new Cat("Pussy", 4));
catList.Add(new Cat("Pelle Svanslös", 3));
catList.Add(new Cat("Disco", 5));
catList.Add(new Cat("Miffo", 1));
catList.Add(new Cat("Pricken", 2));

//Går ej! Klassen Cat implementerar inte IComparable
//catList.Sort();

catList.Sort(new CatComparer()); //Här används egenskapad klass
Console.WriteLine("== Sorterade via CatComparer ==");
foreach (Cat cat in catList)
{
Console.WriteLine(cat.ToString());
}

Console.ReadLine();
}
}

//Egen klass för att möjliggöra anrop till List.Sort
class CatComparer : IComparer<Cat>
{
#region IComparer<Cat> Members

public int Compare(Cat x, Cat y)
{
return x.Name.CompareTo(y.Name);
}

#endregion
}

Sortering med delegaten Comparison<T>
List-klassens Sort-metod har ytterligare en överlagring:

Även denna variant gör det möjligt att sortera objekt av en klass utan att behöva ändra något i själva klassen. Utan att gå in så mycket på vad delegater är, om man skriver in Comparison<T> i Visual Studio, högerklickar och väljer Go to definition... visas följande definition:

public delegate int Comparison<T>(T x, T y)

Comparison<T> är alltså en delegat som kan hantera metoder som returnerar en int och som tar emot två objekt av typen T (metodens namn är valfritt!). Delegaten är generisk, du väljer alltså själv vilken datatyp som T ska vara. Men som du ser ovan i bilden kräver Sort-metoden en Comparison<Cat> och det beror på att listan är en List<Cat>.

Ok, så vi skapar en metod som uppfyller kraven och som dessutom jämför de inskickade objekten;

static int CompareCats(Cat x, Cat y) //Metoden måste vara static
{
return x.Name.CompareTo(y.Name);
}


Nu kan Sort-metoden anropas så här:

catList.Sort(new Comparison<Cat>(CompareCats));

Vi skapar alltså en ny instans av Comparison<Cat> och till den skickar vi in namnet på vår metod.

Det fina med den här varianten är att du behöver inte skapa en helt separat klass och metoden ovan kan helt enkelt placeras i den klass där sorteringen genomförs, t.ex. i den aktuella Form-klassen (WinForms), Page-klassen (Asp.NET) eller direkt i Program-klassen som i detta fall. Och det är ju helt ok om objekt av den här typen endast ska sorteras på endast ett ställe i hela applikationen, annars är IComparer-varianten bättre eftersom en klass är tillgänglig över flera fönster/sidor(=klasser).

Här kommer hela programmet igen:

using System;
using System.Collections.Generic;
using System.Text;

// //////////////////////////////////////////
//Tänk dig att denna klass INTE går att ändra
public class Cat
{
public string Name { get; set; }
public int Age { get; set; }

public Cat(string name, int age)
{
Name = name;
Age = age;
}

public override string ToString()
{
return string.Format("Katt: {0}, {1}", Name, Age);
}
}
// //////////////////////////////////////////

class Program
{
public static List<Cat> catList = new List<Cat>();

static void Main(string[] args)
{
catList.Add(new Cat("Pussy", 4));
catList.Add(new Cat("Pelle Svanslös", 3));
catList.Add(new Cat("Disco", 5));
catList.Add(new Cat("Miffo", 1));
catList.Add(new Cat("Pricken", 2));

catList.Sort(new CatComparer()); //Här används egenskapad klass
printCats("== Sorterade via CatComparer ==");

catList.Sort(new Comparison<Cat>(CompareCats)); //Här används egenskapad metod
printCats("== Sorterade via Comparison<T> ==");

Console.ReadLine();
}

//Egen metod för att möjliggöra sortering via delegaten Comparison<T>
static int CompareCats(Cat x, Cat y)
{
return x.Name.CompareTo(y.Name);
}

//Helper-metod för att skriva ut katterna
private static void printCats(string header)
{
Console.WriteLine(header);
foreach (Cat cat in catList)
{
Console.WriteLine(cat.ToString());
}
Console.WriteLine();
}
}

//Egen klass för att möjliggöra sortering via interfacet IComparer<T>
class CatComparer : IComparer<Cat>
{
#region IComparer<Cat> Members

public int Compare(Cat x, Cat y)
{
return x.Name.CompareTo(y.Name);
}

#endregion
}

IComparable, IComparer, Comparison - what a mess!
Visst kan det vara lite knepigt att hålla isär alla dessa snarlika namn, men om man översätter och tänker efter lite kanske följande definitioner kan hjälpa till:
  • En klass som implemeneterar IComparable<T> gör objekt av klassen jämförbara med varandra; Comparable = Jämförbar
  • En klass som implementerer IComparer<T> har som enda uppgift att kunna jämföra två objekt av samma typ, klassen är en jämförare; Comparer = Jämförare
  • En metod som uppfyller kraven för delegaten Comparison<T> kan genomföra en jämförelse mellan två objekt av samma typ; Comparison = Jämförelse
Att klura på!
Skapa en ny Console Application och ersätt hela Program.cs med koden ovan.
  • Förändra klassen CatComparer så att den kan sortera även på Age (Se tidigare inlägg angående enum). Använd förändringarna för att sortera katterna på ålder.
  • Gör en metod som jämför katterna på Age och som uppfyller kraven för Comparison<T>. Använd den för att sortera katterna på ålder.
  • Som genom ett mirakel får du möjlighet att ändra klassen Cat. Lägg till en katt i catList som heter "Pricken" men som bara är ett år gammal. Implementera IComparable<T> i klassen Cat så att den i första hand jämför på namn och i andra hand på ålder. Anropa Sort-metoden så att kattlistan sorteras enligt detta.
Lösning finns i "the LektionsBloggen Solution".