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".

Boktips och Microsoft E-learning

Gårdagens lektion blev inställd så LektionsBloggen återkommer kring den 4 augusti. Men jag tänkte bjuda på ett boktips istället, trots att jag inte själv är innehavare av boken. En klasskompis har köpt och visade mig boken "Head First C#" från O'Reilly. I utbildningens första C#-kurs hade vi denna bok från O'Reilly och därefter har jag även köpt denna och denna. Och de är verkligen bra! Men deras serie böcker med rubriken "Head First" verkar verkligen vara något alldeles extra.

  Till skillnad från tidigare nämnda böcker är den här boken inte ett ganska torrt referensverk utan snarare en bok med ett extremt pedagogiskt upplägg som verkligen går ut på att läsaren ska lära sig programmering och några sidor i början av boken går igenom smarta tips för studieteknik med mera. Dessutom är boken fylld av övningsuppgifter, korsord och roliga labbar (i den sista gör man en kopia av klassiska spelet Space Invaders[!]). Ett citat ur boken som jag verkligen gillar:

The more you understand, the less you have to memorize.

Exakt sån inställning behövs av någon(person)/något(bok) som ska lära ut. Det är synd att vi inte har denna bok som kursbok, då utbildningen brister en hel del i pedagogiskt upplägg... Blir förstås lite sugen på att köpa den själv, men jag får se. Värt att notera är att Head First-serien även innehåller en mängd andra böcker, bland andra hittade jag en om mjukvaruutveckling i allmänhet som verkar otroligt intressant. Se hela listan på Head First-böcker här.

Ja, för visst kan programmeringsböcker i allmänhet fungera minst lika bra som ett sömnpiller. Igår när jag blev trött på allt läsande kom jag på att jag återigen skulle ge mig på de där Microsoft E-learning-kurserna vi fått tillgång till via skolan. De är ganska bra och det blir lite mer aktivt att sitta och klicka, göra lite små tester och så. Dessutom innehåller de labbar som man genomför genom att via webbläsaren ansluta till en dator där startläget för labben finns. Det gör att labbarna blir lite sega att använda och dessutom lyckas jag trots instruktioner inte ställa om tangentbordsinställningarna så man får trixa lite för att hitta alla specialtecken som behövs i programmeringen; <>;(){}= etc. Så en del av skärmen fick avdelas till ett litet fönster med bild på amerikansk tangentbordslayout som man kan få fram här. (funkar dock endast i IE...) Håll musen över shifttangenten i bilden för att se fler teckens positioner. Men detta krångel till trots lärde jag mig en hel del på WinForms-labben jag gjorde igår kväll... Ska nog ta och göra en till nu.

LektionsBloggen: Möjliggöra sortering av en List<T>

Lektionens datum: 2/7 2008
Lektionens rubrik: Generics, Anonyma funktioner (1/3)
Lektionsbloggens fokus: Hur objekt av en klass kan bli sorteringsbara

Dagens lektion täckte in en hel del och känns minst sagt knepig att sammanfatta. Jag väljer att fokusera på hur objekt av en klass kan bli sorteringsbara.

Kortfattat om Generics
Generics gör det möjligt att implementera en klass som kan hantera objekt av vilken typ som helst. Till exempel kan klassen List<T>, som ingår i .Net Framework, användas för att skapa en lista av vilken slags objekt som helst. Om du har gjort en klass som heter Employee kan du skapa en lista av Employee-objekt enligt följande;

            //Skapa några Employee-objekt
            Employee frida = new Employee("Frida", 10000);
            Employee kalle = new Employee("Kalle", 9000);
            Employee fredrika = new Employee("Fredrika", 12000);

            //Skapa en lista av Employee-objekt
            List<Employee> listOfEmployees = new List<Employee>();
            
            //Lägg till Employee-objekt i listan
            listOfEmployees.Add(frida);
            listOfEmployees.Add(kalle);
            listOfEmployees.Add(fredrika);

Syntaxen som är associerad med Generics är den html-taggliknande konstruktionen där man mellan taggarna ska skriva den typ/klass man vill att den generiska klassen ska hantera, därav List<Employee> Mer om poängen med Generics kan du läsa här och här.

Göra objekt av en klass sorteringsbara så att List<T>.Sort() kan anropas
List<T> innehåller ett antal funktioner som kan användas för att hantera de objekt som ingår i listan, till exempel sortering. Men för att objekt av en viss typ ska kunna sorteras måste klassen implementera ett interface som heter IComparable, detta interface finns dessutom i en generisk version, IComparable<T>, och eftersom generics är bra (läs någon av länkarna ovan, eller någon bok ;-) så använder vi den. För att inte röra ihop det här med WinForms-grejer blir detta exempel en Console Application. Jag kör vidare på mitt djurtema och skapar följande klass:

    class Dog
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public string Breed { get; set; }

        public Dog(string name, int age, string breed)
        {
            Name = name;
            Age = age;
            Breed = breed;
        }

        public override string ToString()
        {
            return string.Format("{0} the {1} is {2} years old.", Name, Breed, Age);
        }
    }

I Main-metoden gör jag sedan följande:

        static void Main(string[] args)
        {
            List<Dog> dogList = new List<Dog>();

            dogList.Add(new Dog("Mozart", 2, "Cocker Spaniel"));
            dogList.Add(new Dog("Barker", 1, "Labrador"));
            dogList.Add(new Dog("Fido", 3, "Poodle"));

            //Här blir det runtime-fel, vad ska en dog sorteras på?
            dogList.Sort();

            foreach (Dog dog in dogList)
            {
                Console.WriteLine(dog.ToString());
            }

            Console.ReadLine();
        }

Jag skapar en lista som hanterar Dog-objekt och lägger till ett antal sådana till listan. Därefter är tanken att listan ska sortas med List-klassens Sort-metod och därefter ska listan skrivas ut till skärmen. Men precis som det står i kommentaren ovan så kommer det här inte att fungera. För hur ska Sort-metoden, som kan hantera precis vilken typ som helst, kunna veta hur objekt av Dog ska sorteras? Ska de sortas på namn, ålder eller ras? Dog måste som sagt implementera interfacet IComparable<T>. Här ser du tilläggen i Dog efter att den ändrats:

    class Dog : IComparable<Dog>
    {
        /.../

        #region IComparable<Dog> Members

        public int CompareTo(Dog other)
        {
            return this.Name.CompareTo(other.Name);
        }

        #endregion
    }

Här ser vi hur klassen Dog implementerat IComparable<Dog>. Interfacet kräver en metod som heter CompareTo, returnerar int och tar emot ett objekt av typen Dog. Denna metod kommer nu att användas av List-klassens Sort-metod när den sorterar objekten. Notera att jag i denna implementation valt att sortera objekten på Name. Det kan jag göra rätt lätt eftersom Name är en string och även string-klassen implementerar IComparable så genom att anropa stringklassens CompareTo-metod kan jag få ut det int-värde som metoden borde returnera. Int-värdet använder förresten Sort-metoden för att avgöra hur objekten ska sorteras, beroende på om värdet är positivt, negativt eller noll. Sådär, då har vi gjort det möjligt att sortera på namn och när programmet nu körs får man följande utskrift:

Barker the Labrador is 1 years old.
Fido the Poodle is 3 years old.
Mozart the Cocker Spaniel is 2 years old.

Men hur ska vi göra om vi även vill att det ska vara möjligt att sortera på ålder? För att hantera detta får vi undersöka de olika överlagringar som finns av List-klassens Sort-metod. En av överlagringarna kräver att man skickar in ett objekt av en typ som implementerar interfacet IComparer<Dog>;

 

Så vi skapar en ny klass, DogComparer, som implementerar detta interface. Eftersom denna klass är hårt knuten till Dog känns det logiskt att nästla denna klass inuti Dog-klassen;

    class Dog : IComparable<Dog>
    {

        public class DogComparer : IComparer<Dog>
        {
            #region IComparer<Dog> Members

            public int Compare(Dog x, Dog y)
            {
                return x.Age.CompareTo(y.Age);
            }

            #endregion
        }

        /.../
    }

Icomparer<Dog> kräver alltså en metod som heter Compare, returnerar en int och tar emot två Dog-objekt. I övrigt skiljer det sig inte nämnvärt från IComparables CompareTo. Nu kan vi göra anropet till sort på något av följande två vis:

            //Alternativ 1
            Dog.DogComparer compareByAge = new Dog.DogComparer();
            dogList.Sort(compareByAge);                    
            
            //Alternativ 2
            dogList.Sort(new Dog.DogComparer());

Enda skillnaden här är tydligheten. I alternativ 1 skapar vi först en referens och skickar sedan in den, i alternativ 2 uttrycks samma sak på en rad. Och eftersom vi aldrig kommer behöva använda referensen compareByAge till någonting annat kan man lika gärna skiva enligt alternativ 2. När vi skickar in DogComparer-objektet i Sort-metoden uppfyller vi alltså överlagringen i bilden ovan, vi skickar in ett objekt som implementerar IComparer<Dog>.

Sist men inte minst ska vi göra så att man också kan sortera på ras. En variant skulle kunna vara att våran DogComparer-klass skulle kunna hetat DogAgeComparer och att vi nu skulle göra ännu en klass som som också implementerar IComparer men som istället heter DogBreedComparer. Men då blir det lätt väldigt många klasser om du tänker dig en klass med betydligt fler fält än dessa tre... En annan variant är att låta en variabel styra vilken sortering som ska göras, och här kommer jag skilja mig lite från det sätt som vår lärare visade idag till förmån för ett sätt jag upptäckte i ett av de exempel som läraren gick igenom (ListDemo, för er som var med ;-). Eftersom det kommer att finnas ett fast antal alternativ för sorteringen verkar det lämpligt att deklarera en enum, CompareType, och sedan låta DogComparer ha en static medlem av CompareType. Dessutom implementerar jag en constructor som tar emot en CompareType, så att man när man skapar ett sådant objekt direkt kan välja vilken sortering man vill ha. Därefter ändrar jag också i CompareTo-metoden så att den tar hänsyn till vilken sortering som ska användas. Hela klassen DogComparer ser nu ut så här:

public class DogComparer : IComparer<Dog>
        {
            //Enum
            public enum CompareType
            {
                ByAge,
                ByBreed
            }

            //Enum-medlem
            public static CompareType CompareBy { get; set; }

            //Constructor
            public DogComparer(CompareType compareBy)
            {
                CompareBy = compareBy;
            }

            public DogComparer() { }    //Om denna konstruktorn anropas kommer CompareBy ha 
                                        //värde ByAge eftersom den är först i enumeratorn

            #region IComparer<Dog> Members

            public int Compare(Dog x, Dog y)
            {
                int returnValue = 0;

                //Ta hänsyn till sorteringsval
                switch (CompareBy)
                {
                    case CompareType.ByAge:
                        returnValue = x.Age.CompareTo(y.Age);
                        break;
                    case CompareType.ByBreed:
                        returnValue = x.Breed.CompareTo(y.Breed);
                        break;
                }

                return returnValue;
            }

            #endregion
        }

Anropet till Sort-metoden kan nu göras så här för att sortera på ålder respektive ras;

            dogList.Sort(new Dog.DogComparer(Dog.DogComparer.CompareType.ByAge));
            dogList.Sort(new Dog.DogComparer(Dog.DogComparer.CompareType.ByBreed));

Här stannar jag för idag. Observera att lektionen i övrigt även tog upp List<T>-metoderna FindAll, ForEach och ConvertAll. Exempel finns i lärarens demo ListDemo. Imorgon fortsätter utbildningen på samma tema.

Ladda hem "the LektionsBloggen Solution"

LektionsBloggen: Delegater och Interface

Lektionens datum: 1/7 2008
Lektionens rubrik: Repetition, Arv, Polymorfism, Delegates And Events, MDI (2/2)
Lektionsbloggens fokus: Delegater och Interface

Dagens lektion handlade om Delegater och Interface. Jag tänkte illustrera dessa genom att utgå från gårdagens exempel.

Dagens blogg blir bildlös då jag sitter i skolan och inte har tillgång till ftp-kontot där jag laddar upp bilderna. Men det blir desto fler kodsnuttar istället, håll till godo! :-)

Filhantering
Om du vill hänga med, ladda hem "the LektionsBloggen Solution", packa upp filen och gör en kopia av mappen 080630_MDI_InheritedForms_Polymorfism. Döp om den kopierade mappen och .csproj-filen så att de heter samma sak, exempelvis "delegateSample". Dubbelklicka sedan på LektionsBloggen.sln, högerklicka på solution-nivån i Solution Explorer och välj Add -> Existing Project..., browsa dig fram till och dubbeklicka på csproj-filen. Högerklicka sedan på detta projekt i Solution Explorer och välj Set as startup project. Sådär, då kan vi köra igång.

Städa bort polymorfismen
För att få ett renodlat exempel kring delegater, tar vi nu bort metoden MakeSound i FrmBase och FrmCat. När du gjort detta, gör en kopia av detta projekt och döp om kopian på samma sätt som ovan till InterfaceSample, jag kommer att använda det senare. Ta nu bort MakeSound även i FrmDog. Ta också bort all kod inne i Timer_Tick1-metoden, vi kommer att lägga till kod där senare.

Delegater
En delegat är en slags datatyp vars innehåll kan peka mot en eller flera metoder. För att förbereda metoder som vår delegat senare ska peka till, implementera följande metod i frmCat:

        public void MakeCatSound(int numberOfTimes)
        {
            Sound += "/ ";
            for (int i = 0; i < numberOfTimes; i++)
            {
                Sound += "Mjau ";
            }
        }

Gör sedan en exakt likadan metod i frmDog men låt den heta MakeDogSound och ändra strängen till Woof . Öppna sedan FrmMainWindow.cs, vårt MDI-fönster. Deklarera en delegat enligt nedan (rad 1) utanför klassen och deklarera en instans av delegaten inuti klassen (rad 5):

    public delegate void MakeSound(int numberOfTimes);

    public partial class FrmMainWindow : Form
    {
        public MakeSound OnMakeSound;

        /.../
    }

Delegatdeklarationen (rad 1) innebär att metoder som ska kopplas till denna delegat måste returnera void och parameterlistan måste bestå av en int. Vilket ju stämmer på metoderna vi nyss la till i frmCat och frmDog. Observera att delegatdeklarationen skulle kunne jämföras med en deklaration av en typ (klass, enum etc.), den har alltså i i sig ingenting att göra just med klassen FrmMainWindow. På rad 5 skapar vi en referens av delegatdatatypen MakeSound som vi kallar OnMakeSound. Denna referens är alltså en medlem i klassen FrmMainWindow. Leta nu upp metoden newCatToolStripMenuItem_Click och lägg till följande rad sist:

OnMakeSound += new MakeSound(cat.MakeCatSound);

Det här är den metod som körs när man väljer menyalternativet "Cat" i File-menyn. På raden ovan lägger vi till att cat-klassens metod MakeCatSound ska köras när OnMakeSound anropas (det anropet återkommer vi till senare). En delegat tilldelas alltid med +=-operatorn för att inte andra metoder som kanske redan tilldelats till delegaten ska bli överskrivna/borttagna. Lägg till en likadan rad i metoden newDogToolStripMenuItem_Click som triggas när man väljer menyalternativet "Dog". Ändra dock cat.MakeCatSound till dog.MakeDogSound. Nu ska vi anropa delegaten OnMakeSound, vi gör det enkelt för oss genom att använda en timer som triggar sig själv varje sekund. Implementera Timer_Tick1 enligt följande;

        Random r = new Random(DateTime.Now.Millisecond);
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (OnMakeSound != null)
            {
                //Slumpa ett tal mellan 1 och 5
                int randomNumber = r.Next(1, 6);
                
                //Anropa de metoder som delegaten pekar till
                OnMakeSound(randomNumber);
            }
        }

Innan man anropar en delegat måste man kolla så att den inte är null, det är ju nämligen helt valfritt för andra klasser om de vill lyssna på en händelse eller inte och i just detta fall kan det vara så att användaren inte öppnat några fönster och i så fall pekar delegaten inte på några metoder. Därefter slumpar vi fram ett tal mellan ett och fem, detta kommer att avgöra hur många gånger varje djurljud skrivs ut. Till sist kommer vi till pudelns kärna, anropet till delegaten; Det ser ut som ett helt vanligt metodanrop där metodnamnet är namnet på den referens vi skapat till delegatdatatypen, och om delegaten tar några argument är det här vi skickar in dom, annars blir det en tom parentes. Det som händer när delegaten anropas nu är alltså att alla instanser av FrmCat och FrmDog anropar sin respektive "make sound"-metod och text skrivs ut i fönstren.

Kombinera delegater och polymorfism
Experimentförslag: Jag kommer inte gå igenom detta så detaljerat, men om du ändrar "make sound"-metoderna i FrmCat och FrmDog så att de heter MakeSound samt deklareras med override, samt implementerar en generell MakeSound-metod i FrmBase som deklareras med virtual, så kan du i metoderna för menyklicken deklarera child-referensen som en FrmBase. Det kommer göra att delegaten anropar MakeSound i FrmBase men eftersom den är virtual och har implementationer även i FrmCat och FrmDog så är det de metoderna som kommer att köras. Se gårdagens exempel kring virtual och override

Interface
Nu utgår vi alltså från projektet interfaceSample där MakeSound-metoden endast finns kvar i FrmDog. Anledningen till detta är ett litet trick man kan göra för att skapa ett interface. Öppna FrmDog i kodläge och ta bort override på deklarationen av MakeSound (Det finns ju ingen virtual metod i basklassen längre). Nu till interface-tricket; högerklicka i textytan och välj Refactor -> Extract interface. En dialogruta öppnas där du får namnge interfacet och välja vilka medlemmar i den aktuella klassen som ska kopplas till interfacet. Döp interfacet till IAnimalWithSound och kryssa för MakeSound. Nu skapas helt automatiskt filen IAnimalWithSound.cs med följande innehåll:

using System;
namespace MDI
{
    interface IAnimalWithSound
    {
        void MakeSound();
    }
}

Vi har alltså skapat ett interface som kräver att klasser som implementerar detta interface innehåller en metod som heter MakeSound, returnerar void och inte har några parametrar. Kolla på klassdeklarationen i FrmDog, där har implementationen av MakeSound redan lagts till:

public partial class FrmDog : MDI.FrmBase, MDI.IAnimalWithSound

(Du kan ta bort MDI. på både klassen som ärvs och interfacet som implementeras) Gå nu till FrmCat och skriv in , IAnimalWithSound sist i klassdeklarationen. En liten markering visas under I:et, klicka på den och välj Implement interface 'IAnimalWithSound'. Följande kod genereras ut i filen:

        #region IAnimalWithSound Members

        public void MakeSound()
        {
            throw new NotImplementedException();
        }

        #endregion

Ändra metoden så att den ser ut så här:

        public void MakeSound()
        {
            Sound += "Mjau ";
        }

Nu kommer vi kunna behandla våra fönster oavsett om de är av typen FrmCat eller FrmDog som en IAnimalWithSound. Implementera Timer1_Tick-metoden i FrmMainWindow enligt följande;

        Random r = new Random(DateTime.Now.Millisecond);
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (this.MdiChildren.Length > 0)
            {
                //Slumpa ett tal mellan 0 och antal öppna fönster
                int randomNumber = r.Next(0, this.MdiChildren.Length);

                IAnimalWithSound randomWindow = (IAnimalWithSound)this.MdiChildren[randomNumber];
                randomWindow.MakeSound();
            }
        }

Eftersom vi vet att alla fönster i MdiChildren implementerar IAnimalWithSound kan vi typa om det till ett IAnimalWithSound. Därefter kan vi anropa MakeSound eftersom vi vet att alla klasser som implementerar IAnimalWithSound har en sån metod. Och ja, det är väl ungefär det som är pudelns kärna när man använder sig av interface.

Tankar kring skillnader mellan polymorfism, interface och delegater
Jag är inte helt på det klara med när man egentligen ska använda vad, men en skillnad man kan konstatera är ju att delegatvarianten aldrig ställer några krav på vad metoder den triggar ska heta, den ställer inte heller några krav på att nån överhuvudtaget måste "lyssna" på den, så den känns lite "lösare" på sätt och vis. När det gäller polymorfism så är ju det endast aktuellt om det finns en arvsrelation mellan klasser, så har man inte det men ändå behöver behandla objekt av olika klasser på ett generellt sätt så känns interface som the way to go. När det gäller just mitt exempel känner jag polymorfismens override och virtual känns mest rätt, just för att jag har en arvsrelation mellan klasserna. Jag lär återkomma kring dessa funderingar här på bloggen.

I morgon står det "Generics, anonyma funktioner (1/3)" på schemat. Lektionsbloggen återkommer kring detta!

Ladda hem "the LektionsBloggen Solution"