István's profileNovák IstvánPhotosBlogListsMore Tools Help

Novák István

"Dive deeper"
March 26

LVN! Sidebar #2 - Resolving string resources

If you used the VSPackage wizard—and I am sure you used it by now—you can discover that it creates a VSPackage.resx file and also a Resources.resx file to store localizable resource information. Attributes used for package registration have indirect references to strings in the VSPackage.resx file. A good example is the InstalledProductRegistration attribute:

[InstalledProductRegistration(false, "#110", "#112", "1.0", IconResourceID = 400)]

public selaed class MyPackage: Package { ... }

The highlighted parameters of the attribute are indirect references to the strings with the ID of 110 and 112 in the VSPackage.resx file. I like this mechanism, so I decided to create a similar behavior to use it in my packages. In this blog post I will show you how I have done it.

Referencing string resources

We can put resources either into the VSPackage.resx or to the Resources.resx file (and even to other .resx files). By the way, why to have two .resx files? In the current implementation of VS SDK, the infrastructure in Visual Studio Shell (do not forget it is based on COM technology!) can co-operate with resources in the VSPackage.resx file. The build targets used by VSPackages embed the resources in VSPackage.resx file so that Visual Studio Shell can access them. Sorry, but in this blog post I am not going to explain how it is done...

We like Resources.resx file since the ResXFileCodeGenerator custom tool attached to the file generates code where resources can be accessed through properties.

I decided to create a string resolution mechanism where you can resolve strings in localizable resources either declared in VSPackage.resx or in Resources.resx. I have created a generic class called StringResolver having the following blueprint:

public static class StringResolver<TPackage>

  where TPackage: Package

{

  public static string Resolve(string toResolve);

  public static bool ExistsInResourcesClass(string key);

  public static string ResolveInResourcesClass(string key);

  public static bool ExistsInPackageResources(string key);

  public static string ResolveInPackageResources(string key);

  public static Type GetResourcesClassType();

}

The Resolve method is intended to be used most of the time. This method accepts a string parameter and returns with a string resolved by the input parameter. If the input string starts with “#”, the trailing part is treated as a string resource key in VSPackages.resx. Should the input string begin with “$”, the trailing part is handled as a property name in the Resources class corresponding to the class generated by the ResXFileCodeGenerator custom tool from the Resources.resx file. In any other cases the method retrieves the input string.

I provided the ResolveInResourcesClass and ResolveInPackageResources static helper methods to search for resource strings within target locations referenced in method names. The ExistsInResourcesClass and ExistsInPackageResources methods are provided for pre-checking resources before accessing them.

Implementation details

The StringResolver class expects a type parameter of TPackage that must be an inheritor of the Package class. Why is it so? To find package resources we have to use the IVsResourceManager (interop) interface to call its LoadResourceString method where we have to pass the GUID of the current package as an input parameter. If we had only one package class in our package assembly (as generally we have) we could scan the types with Reflection to find the package class and take its GUID. However, it is allowed to put more than one package into the assembly and in this case scanning through types would not work.

Actually I use scanning through types to obtain the Resources class (generated by ResXFileCodeGenerator). The class is used when we want to resolve a string resource within Resources.resx. It is not a resource-saving behavior to scan for the Resources class every time we have to resolve a string, so I decided to add a caching mechanism to StringResolver.

Finding the Resources class

StringResolver is in a separate utility class library (VsxLibrary). When we create one or more packages using VsxLibrary, each package intends to use Resource.resx in its own assembly. For that reason we address the corresponding Resources classes through the containing assembly. Here is the code for the GetResourcesClassType method that obtains the appropriate Resources class for us:

public static class StringResolver<TPackage>

  where TPackage: Package

{

  private static readonly Dictionary<Assembly, Type> _ResourceTypes =

      new Dictionary<Assembly, Type>();

  // ...

 

  public static Type GetResourcesClassType()

  {

    Assembly callingAsm = typeof(TPackage).Assembly;

 

    // --- Check the cache for Resources type and return if found.

    Type resourceType;

    if (_ResourceTypes.TryGetValue(callingAsm, out resourceType))

    {

      return resourceType;

    }

 

    // --- Search for the Resources type. If found add it to the cache.

    foreach (Type type in callingAsm.GetTypes())

    {

      if (type.GetCustomAttributes(typeof(CompilerGeneratedAttribute),

        false).Length > 0 && !type.IsVisible && type.Name == "Resources")

      {

        _ResourceTypes.Add(callingAsm, type);

        return type;

      }

    }

    return null;

  }

  // ...

}

 

Instead of every time scanning for the Resources class in the package assembly, we store them in a cache addressed by the package assembly itself. If we do not find Resources class for the assembly in the cache, we scan through the compiler generated classes with the name of “Resources” in the assembly and retrieve the first we found.

Obtaining resource properties

Having the Resources class it is quite easy either to obtain the resource by the corresponding property name or check if the property exists. This is the responsibility of ResolveInResourcesClass and ExistsInResourcesClass methods:

// ...

public static bool ExistsInResourcesClass(string key)

{

  Type resourceType = GetResourcesClassType();

  if (resourceType == null) return false;

  PropertyInfo propInfo = resourceType.GetProperty(key, BindingFlags.Static |

    BindingFlags.NonPublic);

  return propInfo != null;

}

// ...

public static string ResolveInResourcesClass(string key)

{

  Type resourceType = GetResourcesClassType();

  if (resourceType == null) return Resources.ResourceNotFound;

  PropertyInfo propInfo =

    resourceType.GetProperty(key, BindingFlags.Static | BindingFlags.NonPublic);

  if (propInfo == null) return Resources.ResourceNotFound;

  return propInfo.GetValue(null, null).ToString();

}

// ...

Obtaining package resources

In Visual Studio Packages the SVsResourceManager service is the one that provides methods to access package resources through its IVsResourceManager interface. We use only the LoadResourceString method. Using this interface is quite straightforward:

// ...

public static bool ExistsInPackageResources(string key)

{

  Guid packageGuid = GetPackageGuid();

  string resourceString;

  IVsResourceManager resourceManager =

    (IVsResourceManager)Package.GetGlobalService(typeof(SVsResourceManager));

  if (resourceManager == null) return false;

  int result = resourceManager.LoadResourceString(ref packageGuid, -1, key,

    out resourceString);

  return result == VSConstants.S_OK;

}

// ...

public static string ResolveInPackageResources(string key)

{

  Guid packageGuid = GetPackageGuid();

  string resourceString;

  IVsResourceManager resourceManager =

    (IVsResourceManager) Package.GetGlobalService(typeof (SVsResourceManager));

  if (resourceManager == null) return Resources.PackageNotFound;

  int result = resourceManager.LoadResourceString(ref packageGuid, -1, key,

    out resourceString);

  if (result != VSConstants.S_OK) return Resources.PackageNotFound;

  return resourceString;

}

// ...

private static Guid GetPackageGuid()

{

  return typeof(TPackage).GUID;

}

// ...

Putting the pieces together

Now, we have every piece to create the Resolve method:

public static string Resolve(string toResolve)

{

  if (string.IsNullOrEmpty(toResolve) || toResolve.Length < 2) return toResolve;

  if (toResolve.StartsWith("#"))

  {

    toResolve = toResolve.Substring(1);

    return toResolve.StartsWith("#")

      ? toResolve : ResolveInPackageResources(toResolve.Trim());

  }

  else if (toResolve.StartsWith("$"))

  {

    toResolve = toResolve.Substring(1);

    return toResolve.StartsWith("$")

      ? toResolve : ResolveInResourcesClass(toResolve.Trim());

  }

  return toResolve;

}

As you see from the code we can use “##” and “$$” if we would like the first “#” or “$” taken into account as literals instead of resolver marks.

Using StringResolver

When you create a package, it is quite easy to use StringResolver.  Here I do not show you a full source code but only a small code snippet good enough to imagine how to resolve strings:

public selaed class MyPackage: Package

{

  // ...

  protected override void Initialize()

  {

    // ...

    string myWindowCaption = StringResolver<MyPackage>.Resolve("$WindowTitle");

    // ...

  }

  // ...

}

In the next LVN! Sidebar I show you some more examples of using StringResolver.

VS 2008 Discrepancies – # 1: ElementHost behavior at design time

As soon as Visual Studio 2008 RTM appeared on MSDN I downloaded and installed it. Right now I am using it to test VSTO 2008. Recently I have created an Excel ActionPane using a WPF user control I. To be able to display the WPF user control in an ActionPane — that is actually a WinForms user control — I used the ElementHost control. I docked the ElementHost control in the parent ActionPane control and assigned the WPF user control to the ElementHost. My original WPF user control looks like this:

 

I expected the ActionPane showing the same user control surface but it displayed the following one:

Where has the missing — gray part — of my user control gone? Hmm. About the 20% of the right and the top area is missing… What can be wrong?

Then the light turned on! I am using my Vista with 120 DPI screen resolution. Let’s try to go back to 96 DPI (Default) setting. Surprise! My user control displays its full area:

 

So let us make another try! I set the resolution to 192 DPI (do not do that on your machine) I expected that only the top left quarter of my user control can be seen. I got what I was waiting for:

 

So bad news: the ElementHost control does not handle the embedded WFP user control’s measurement correctly at design time. There is good news: when running the application, ElementHost seems to work properly — mean, does not gray out my WPF user control.

To avoid this phenomenon right now the only solution seems to use the default 96 DPI resolution in Vista.

June 25

A LINQ over C# projekt

A LINQ-kel való ismerkedés során elindítottam egy „kísérleti” nyílt forráskódú projektet a CodePlex-en. A projekt egyelőre még nem publikus, július közepére tervezem, hogy megnyitom. Várom azok visszajelzését, jelentkezését, akik résztvennének ebben a kísérletben. A projekt eredményeinek egy kis részét megpróbálom bemutatni a június 25-ei Platform Innovációs Napokon, mielőtt megkezdeném rendes nyári szabadságomat, amelyről július 9-én térek vissza.

Két dolog kapcsán tekintem kísérletinek a projektet:

Ø  Szeretném kideríteni a LINQ használhatóságát (erősségeit, gyengeségeit), miközben meglehetősen mélyen megismerkedem annak implementációs részleteivel.

Ø  Kiváncsi vagyok, hogy tud-e itt nálunk egy teljesen közösség nyílt forráskódú projekt működni.

A LINQ over C# projekt lényege olyan LINQ kiterjesztés (provider) elkészítése, amely egy C# projekt állományait (a benne lévő projekt fájlt és kódállományokat) olvasva közvetlenül a projekt, illetve a kód elemeit le tudja kérdezni. (A háttérben egy C# szintaktikai és szemantikai elemző fog állni, amely nagyjából minden tud, amit egy C# fordítóprogram, de MSIL kódot nem bocsát ki.)

A projekt még alpha állapotban van, de már teljes egészében működik a szintaktikai elemzés (C# 2.0). Még rengeteget kell rajta fejleszteni és hangolni, de az eddigi eredmények biztatóak.

A projekt során nyílt forráskódú C# projektek forráskódján tesztelem a rendszert (pl. NUnit, CSLA).

Az alábbiakban bemutatok néhány ilyen lekérdezést, amelyeket az Orcas Beta 1-el hajtottam végre egy virtuális gépen. A lekérdezéshez a CSLA projekt forráskódját használtam fel.

A C# projekt szintaktikai elemzése

Az alábbi programrészlet a megadott könyvtárban lévő C# projekt összes forrásfájljának szintaktikai elemzését végzi el. A projekt későbbi változataiban ennek explicit leírására már nem lesz szükség:

const string CSLAFolder =

  @"C:\Work\LINQ-CSF\CSharpParser\CSharpParserTest\LargeTestProjects\CSLA";

// ...

ProjectParser parser = new ProjectParser(CSLAFolder, true);

ParserHost.InvokeParser(parser);

A CSLA projekt 85 forrásfájlja esetében a teljes szintaktikai elemzés kb. 280 ms-ot vett igénybe.

Egy C# projektben található fájlok lekérdezése

var files = from file in parser.Files

            select new { Name = file.Name };

A lekérdezés 2 ms alatt adta vissza az érintett 85 fájl nevét.

A névterek lekérdezése

Az alábbi lekérdezés visszaadja a projektben található összes névteret és azt, hogy melyik hány részletben került definiálásra (minden fájlban található definíció egy önálló részlet):

var namespaces =

    from ns in parser.DeclaredNamespaces.Keys

    orderby parser.DeclaredNamespaces[ns].Count descending

    select new { Name = ns, Count = parser.DeclaredNamespaces[ns].Count };

A lekérdezés eredménye:

Query took 5 ms

Name                  Count

Csla                  23

Csla.Core             19

Csla.Validation       13

Csla.Server           9

Csla.Security         7

Csla.DataPortalClient 5

Csla.Data             3

Csla.Server.Hosts     3

Number of items: 8

Az összes típus lekérdezése

Az alábbi lekérdezés visszaadja az összes típust, amelyet a projekt definiál:

var typesDeclared =

  from type in parser.GetAllTypes()

  orderby type.Name

  select new { type.ParametrizedName };

A generikus típusok összegyűjtése

Az előző lekérdezés egy kis módosítással a generikus típusokat adja vissza:

var genericClasses =

  from type in parser.GetAllTypes()

  where type.IsGenericType && type is ClassDeclaration

  orderby type.Name

  select new { type.ParametrizedName };

Eredmény:

Query took 3 ms

BusinessBase<T>

BusinessListBase<T, C>

EditableRootListBase<T>

ExtendedBindingList<T>

FilteredBindingList<T>

NameValueListBase<K, V>

ReadOnlyBase<T>

ReadOnlyBindingList<C>

ReadOnlyListBase<T, C>

RuleMethod<T, R>

SortedBindingList<T>

Number of items: 11

A legtöbb metódust tartalmazó típusok

Egy összetettebb lekérdezéssel megvizsgálhatjuk, hogy mely típusok tartalmazzák a legtöbb metódust:

var methodStat =

  from methodInfo in

    (

      from type in parser.GetAllTypes()

      select new

      {

        Name = type.Name,

        Count = (from method in type.Members

                 where method is MethodDeclaration

                 select method).Count()

      }

    )

  orderby methodInfo.Count descending

  select methodInfo;

Eredmények (részlet):

Query took 7 ms

Name                      Count

SmartDate                 52

SafeDataReader            52

BusinessBase              50

BusinessListBase          40

FilteredBindingList       36

SortedBindingList         33

ValidationRules           31

ReadOnlyBase              26

DataPortal                18

AuthorizationRules        17

EditableRootListBase      16

NameValueListBase         16

...

RuleHandler               0

RuleSeverity              0

Number of items: 85

A DataPortal osztály legtöbb utasítást tartalmazó metódusai

Az előző lekérdezések még olyanok voltak, hogy azokat akár a Reflection típusok segítségével egy lefordított C# assemblyből is le tudtuk kérdezni (forráskódból azért nem!). Ezt a lekérdezést már csak a forráskód elemzésével tudjuk megtenni:

var complexMethods =

  from complexInfo in

    (

      from type in parser.GetAllTypes()

      where type.Name == "DataPortal" && type.IsClass &&

        (type as ClassDeclaration).IsStatic

      from method in type.Members

      where method is MethodDeclaration

      select new { method.Name,

        Count = (method as MethodDeclaration).AllStatements().Count() }

    )

  orderby complexInfo.Count descending

  select complexInfo;

Eredmények:

Query took 23 ms

Name                       Count

Update                     18

Create                     13

Fetch                      13

Delete                     12

GetDataPortalProxy         5

OnDataPortalInvoke         3

OnDataPortalInvokeComplete 3

GetPrincipal               3

...

A „result” lokális változó előfordulási helyei

A mintapéldákat egy olyan lekérdezéssel zárom, amely kiírja, hogy mely típusok melyik metódusai definiálnak „result” nevű lokális változókat, illetve a kapcsolódó forrásfájl melyik sorában teszik ezt:

var resultAsLocalVar =

  from methodInfo in

    (

      from type in parser.GetAllTypes()

      from method in type.Members

      where method is MethodDeclaration

      select new { type.Name, Method = method as MethodDeclaration }

    )

  from statement in methodInfo.Method.Statements

  where statement is LocalVariableDeclaration &&

    (statement as LocalVariableDeclaration).Name == "result"

  select new

  {

    TypeName = methodInfo.Name,

    MethodName = methodInfo.Method.Name,

    Line = statement.StartLine

  };

A lekérdezés eredménye:

Query took 12 ms

TypeName                 MethodName          Line

BusinessBase             Save                146

BusinessListBase         Save                601

DefaultFilter            Filter              11

FilteredBindingList      AddNew              215

FilteredBindingList      FilteredIndex       882

ReadOnlyBase             CanReadProperty     193

ReadOnlyBase             CanReadProperty     211

ReadOnlyBase             CanReadProperty     253

SortedBindingList        AddNew              250

SortedBindingList        SortedIndex         796

Utilities                GetChildItemType    87

BusinessBase             CanReadProperty     388

BusinessBase             CanReadProperty     406

...

CommonRules              MinValue            319

SharedValidationRules    GetManager          28

ValidationRules          GetRuleDescriptions 109

Number of items: 51

Kattanj rá a LINQ-re! - 1 rész: A jéghegy csúcsa

Már közel egy éve a legtöbb Microsoft fejlesztői konferencia nem engedheti meg magának, hogy legalább egyszer meg ne említse a „LINQ”-et, mint a C# 3.0-ás változatának alapvető újítását. Csak nemrégiben kezdett a köztudatban is letisztulni, hogy mindaz, amit a .NET 3.5-höz és az „Orcas”-hoz kapcsolódóan nyelvi újításként emlegetett a Microsoft, valójában nem egyszerűen néhány új C# nyelvi elemet jelent.  A .NET keretrendszer egy olyan új technológiával gazdagodott, amely alapvető információkezelési feladataink során az SQL nyelvhez hasonló módon deklaratívvá teszi adatok összegyűjtését, lekérdezését.

Bár a LINQ a Language Integrated Query (hevenyészett fordításban a „nyelvbe ágyazott lekérdezések”) megnevezésből alkotott mozaikszó, a technológia lényege számos ötletes —nyugodtan használhatom a „zseniális” jelzőt — elgondoláson és a fordítóprogrammal való szoros integráción alapszik.

Ebben a cikksorozatban körbejárom a LINQ alapjait, bemutatom a technológia legfontosabb jellemzőit. Néhány ponton benézünk majd a „kulisszák mögé” is, hogy jobban megismerhessük mi is van a háttérben.

Lépések a LINQ felé

A LINQ-et a bevezetőben úgy említettem, mint a .NET 3.5 egy új technológiája az adatlekérdezési, információfeldolgozási feladatok megvalósításának támogatására. Hogyan ad ez a technológia értéket a fejlesztők mindennapi tevékenységéhez? Ezt a kérdést talán úgy vizsgálhatjuk meg, ha némi betekintést nyerünk a technológia működési alapjainak néhány részletébe. Ebben a fejezetben lépésről lépésre bemutatom egy viszonylag egyszerű adatfeldolgozási folyamat során azt, hogy a jelenleg használt megoldási minták irányából hogyan juthatunk el a LINQ teljes szolgáltatáshalmazának kiaknázásáig.

Legyen a feladatunk egy olyan jelentés előálltása, amely ügyfelek és a hozzájuk tartozó rendelésállomány alapján területenként előállítja az értékesítési statisztikát, vagyis azt, hogy mely területen (településen) mennyi volt a teljes és rendelésenkénti átlagos értékesítés.

Adatfeldolgozás „hagyományos” módon

Alkalmazásunkban az adatréteget a Customer és Order típusok képviselik, amelyek az ügyfelek és a hozzájuk tartozó rendelések adatait személtetik:

namespace LINQDemo.Introduction

{  public class Customer

  {

    public int Id;

    public string Name;

    public string City; 

    public Customer(int id, string name, string city)

    {

      Id = id;

      Name = name;

      City = city;

    }

  } 

  public class Order

  {

    public int Id;

    public int CustomerId;

    public double Amount; 

    public Order(int id, int customerId, double amount)

    {

      Id = id;

      CustomerId = customerId;

      Amount = amount;

    }

  }

}

Szintén entitásként jelenik meg a ReportItem típus, amely az értékesítési statisztika egy bejegyzését tartalmazza:

namespace LINQDemo.Introduction

{

  public class ReportItem

  {

    public string City;

    public int Count;

    public double Sum;

    public double Avg { get { return Sum / Count; } } 

    public ReportItem(string city, int count, double sum)

    {

      City = city;

      Count = count;

      Sum = sum;

    }

  }

}

Az alkalmazás üzleti funkciói (jelenleg ez a riport elkészítése) A BusinessLogic típusban kapnak helyet.

namespace LINQDemo.Introduction

{

  public static class BusinessLogic

  {

    public static List<ReportItem> GetSalesInfo(List<Customer> customers,

      List<Order> orders)

    {

      ...

    }

  }

}

Az Application típus az alkalmazásunk megjelenítési rétegét, illetve a vezérléséhez szükséges logikát tartalmazza:

namespace LINQDemo.Introduction

{

  public static class Application

  {

    private static List<Customer> Customers = new List<Customer>();

    private static List<Order> Orders = new List<Order>(); 

    public static void InitData()

    {

      ...

    } 

    public static void GetSalesInfo()

    {

      List<ReportItem> salesData = BusinessLogic.GetSalesInfo(Customers, Orders);

      foreach (ReportItem item in salesData)

      {

        Console.WriteLine("{0,-20}{1,-10}{2,-10}", item.City, item.Sum, item.Avg);

      }

    }

  }

}

Most, hogy alkalmazásunk rétegei adottak, építsük fel azt az üzleti logikát, amely az értékesítési statisztikát előállítja. Ha ezt a jelentést egy adatbázisból kellene előállítanunk, ahol a Customer és Order entitásoknak megfelelő táblák megtalálhatók, valahogy így írnánk le az ehhez szükséges SQL lekérdezést:

select

  Customer.City, sum(Order.Amount), avg(Order.Amount)

from

  Customer inner join Order

    on Customer.Id = Order.CustomerId

group by Customer.City

Ugyanezt a logikát imperatív módon is leírhatjuk, rengeteg megvalósítás lehetséges. Én az alábbi megoldást választottam:

public static List<ReportItem> GetSalesInfo(List<Customer> customers,

  List<Order> orders)

{

  // --- Index az ügyfelek gyors eléréséhez

  Dictionary<int, Customer> customerIndex = new Dictionary<int, Customer>();

  // --- A számításhoz használt munkaterület

  Dictionary<string, ReportItem> work = new Dictionary<string,ReportItem>();

  foreach (Customer customer in customers)

  {

    // --- Indexet készítünk az ügyfelek elérésére Id alapján

    customerIndex.Add(customer.Id, customer);

    // --- Létrehozzuk a munkaterületet a riport készítéséhez

    if (!work.ContainsKey(customer.City))

    {

      work.Add(customer.City, new ReportItem(customer.City, 0, 0.0));

    }

  } 

  // --- Elkészítjük a riportot

  foreach (Order order in orders)

  {

    string city = customerIndex[order.CustomerId].City;

    ReportItem item = work[city];

    item.Count++;

    item.Sum += order.Amount;

  } 

  // --- Az eredményt az elvárt formában adjuk vissza

  return new List<ReportItem>(work.Values);

}

Hogyan is viszonyul az imperatív kóddal leírt megoldásunk a fejezetrész elején leírt SQL-szerű megoldáshoz? Nem vitatható, hogy az SQL-szerű leírás rövidebb és érthetőbb is. Ugyanakkor a .NET jelenlegi eszközeivel (a .NET alapobjektum-könyvtár segítségével) nekünk csak egy a fent vázolthoz hasonló megoldás marad. A „kézi” megoldás kialakítása során több olyan részletre is figyelnem kellett, amelyeket egyébként egy adatbáziskezelő eszköz az SQL lekérdezések segítségével saját hatáskörében megold:

Ø  Indexet készítettem a Customer példányokhoz (customerIndex), hogy az egyes ügyfelek az Id tulajdonságukon keresztül egyszerűen elérhetők legyenek. Ha ezt nem tettem volna, egy ciklus segítségével kellene megtalálnom egy adott azonosító alapján a hozzá tartozó ügyfélobjektumot.

Ø  Önálló lépésben létrehoztam egy munkatáblát  (work), amely a jelentés elemeinek aggregációját segíti.

Ø  A rendelésállomány elemeit egyenként végigjárva előállítottam a kívánt eredményhalmazt.

Ø  Az eredményhalmaz egy közbenső, az aggregáláshoz használt formában állt elő, azt olyan formára (List<ReportItem>) kellett hozni, amely megfelel az üzleti funkció elvárt kimenetének.

A működés ellenőrzéséhez az z Application típus InitData metódusában töltsük fel mintaalkalmazásunkat adatokkal!

public static void InitData()

{

  Customers.Add(new Customer(1, "Gipsz Jakab", "Karcag"));

  Customers.Add(new Customer(2, "Ragta Pasz", "Dunakeszi"));

  Customers.Add(new Customer(3, "Vég Béla", "Gyöngyös"));

  Customers.Add(new Customer(4, "Szőke Barna", "Dunakeszi"));

  Customers.Add(new Customer(5, "Barna Piros", "Karcag"));

  Customers.Add(new Customer(6, "Gyár Telep", "Dunakeszi"));

  Customers.Add(new Customer(7, "Mikro Szoft", "Gyöngyös"));

  Customers.Add(new Customer(8, "Cserép Virág", "Dunakeszi"));

  Orders.Add(new Order(1000, 1, 110));

  Orders.Add(new Order(1001, 2, 4500));

  Orders.Add(new Order(1002, 1, 320));

  Orders.Add(new Order(1003, 3, 2400));

  Orders.Add(new Order(1004, 1, 245));

  Orders.Add(new Order(1005, 7, 2345));

  Orders.Add(new Order(1006, 4, 112));

  Orders.Add(new Order(1007, 2, 214));

  Orders.Add(new Order(1008, 5, 5410));

  Orders.Add(new Order(1009, 6, 216));

  Orders.Add(new Order(1010, 8, 3210));

  Orders.Add(new Order(1011, 3, 237));

  Orders.Add(new Order(1012, 4, 119));

  Orders.Add(new Order(1013, 3, 118));

  Orders.Add(new Order(1014, 8, 678));

  Orders.Add(new Order(1015, 2, 551));

  Orders.Add(new Order(1016, 6, 24));

}

Az alkalmazást futtatva (Az Application típus InitData és GetSalesData metódusainak egymás utáni hívásával) az alábbi eredményeket kapjuk:

Karcag              6085      1521.25

Dunakeszi           9624      1069.33333333333

Gyöngyös            5100      1275

(Az első oszlop a város neve, a második a teljes, a harmadik oszlop pedig az átlagos rendelésállomány).

Egy nagyobb alkalmazás esetében nem egy ilyen kis üzleti funkciót kell elkészítenünk, hanem általában néhány tucatot, esetleg nagyságrendekkel többet. Minden olyan megvalósítás, amely egy-egy funkció megvalósításához extra programsorokat ad hozzá, hatványozottan jelentkezik ha egy egész halom üzleti funkcióról van szó.

A legtöbb fejlesztőcsapat eljut odáig, hogy a gyakran előforduló — több kódsort igénylő, összetett — adatlekérdezési, feldolgozási tevékenységekhez, alapfunkciókhoz újrahasznosítható kódkönyvtárat készít és azt használja az üzleti funkciók megvalósítása során.

A System.Linq névtér használata

A Microsoft mérnökei Anders Hejlsberg és Don Box irányításával a LINQ projekt keretében elkészítették az újrahasznosítható kódkönyvtárat —emellett még számtalan egyéb fontos technológiai elemet is —, amelyet a lekérdezések során használhatunk. Figyelem: nem lehet eléggé hangsúlyozni, hogy ez csak a projekt egyik eleme! Bár a LINQ-et mindenki a .NET 3.5-tel köti össze, fontos tudnunk, hogy az elkészült kódkönyvtárat akár a .NET 2.0-val és a Visual Studio 2005-tel együtt is használhatjuk.

Az említett funkciók a System.Core.dll-ben, a System.Linq névtérben találhatók. A funkciókönyvtár definiálja az ún. „standard query operators” absztrakt fogalomrendszert és erre építi fel az adatlekérdezések, adatfeldolgozások szemantikáját. Az operátorok között megtaláljuk a rendezést, kiválasztást, csoportosítást, projekciót és a különböző aggregációkat, illetve ezek kombinációit. Az operátorok alapvetően az ún. delegate típusok és iterátorok (IEnumerable leszármazottak) segítségével működnek.

A Visual Studio 2005 és a .NET 2.0 segítségével elkészítettem a GetSalesInfo olyan változatát, amely a System.Linq névtér típusait használja a jelentés elkészítésére:

using System.Linq;

// ...

public static List<ReportItem> GetSalesInfo(List<Customer> customers, List<Order> orders)

{

  Func<Customer, int> customerPkSelector = delegate(Customer c) { return c.Id; };

  Func<Order, int> orderFkSelector = delegate(Order o) { return o.CustomerId; };

  Func<Customer, Order, CityOrder> cityOrderSelector =

    delegate(Customer c, Order o) { return new CityOrder(c.City, o.Amount); };

  IEnumerable<CityOrder> cityOrders =

    Enumerable.Join(customers, orders, customerPkSelector, orderFkSelector,

    cityOrderSelector);

  Func<CityOrder, string> keySelector = delegate(CityOrder o) { return o.City; };

  IEnumerable<IGrouping<string, CityOrder>> cityGroup =

    Enumerable.GroupBy(cityOrders, keySelector);

  Func<CityOrder, double> amountSelector = delegate(CityOrder o) { return o.Amount; };

  Func<IGrouping<string, CityOrder>, ReportItem> resultSelector =

    delegate(IGrouping<string, CityOrder> cog)

    {

      return new ReportItem(cog.Key, Enumerable.Count(cog),

      Enumerable.Sum(cog, amountSelector));

    };

  IEnumerable<ReportItem> result = Enumerable.Select(cityGroup, resultSelector);

  return new List<ReportItem>(result);

}

Hát bizony, az eredmény — fogalmazzunk így — „érdekes”. A lekérdezés előállítására használt kódkönyvtár nem eredményezett olvashatóbb kódot, mint a korábbi „kézi” változat. A kódot jobban megnézve még az is kiderül, hogy a feldolgozás során egy CityOrder nevű típust is felhasználtam, amely az egyes városokhoz kapcsolódó megrendeléstételeket tartalmazza:

namespace LINQDemo.Introduction

{

  public class CityOrder

  {

    public string City;

    public double Amount; 

    public CityOrder(string city, double amount)

    {

      City = city;

      Amount = amount;

    }

  }

}

A kód intenzíven használ névtelen delegate típusokat, amelyek tulajdonképpen a lekérdezéshez tartozó néhány elemi műveletet definiálják. A kódot megfigyelve észrevehetjük, hogy abban nem szereplenek ciklusok, ellenben találunk egy Enumerable osztályt, amelynek olyan nevű műveleteit használjuk, mint például Join, GroupBy, Count, Sum, stb.

Ha ezen a ponton az Olvasót arról igyekezném meggyőzni, hogy „ugyan hosszabb és nehezebben olvasható ez a mintapélda, de a megoldás szép...” valószínűleg sokan javasolnák az elmeorvos mihamarabbi felkeresését.  Nem szeretnék orvoshoz menni, és igyekszem megmutatni, hogy a fenti megoldás most még ugyan frusztráló, de a szépség igenis „előrángatható” belőle...

A C# 3.0 használata

Lehet, hogy a fenti a fenti System.Linq névteret használó megoldásban azért ilyen csúnya, mert a LINQ projektcsapat ilyen „gyatra” megoldást „kalapált össze”? Nem, erről szó sincs... Ha itt és most megnéznénk, pontosan hogyan is működik ez a megoldás —de ezt itt most nem tesszük —, felismernénk, hogy letisztult  és alapvetően szép elgondolás áll a háttérben. Akkor vajon mégis mi frusztrál bennünket a példa kapcsán?

A megoldás elemei rengeteg ún. „szintaktikai zajt” tartalmaznak. Ezek közül a legjellemzőbb az, hogy rengeteg névtelen delegate típust használ, amelyek leírásánál bizony sok olyan ismétlődő, a leírásmód kapcsán valójában értéktelen dolog van, amelyet egyszerűen csak a C# nyelv szigorú szintakszisa követel meg. Az alábbi részletben kiemelt színnel jelöltem meg azokat a kódrészleteket, amelyek értéket hordoznak, a többi ebben az értelemben a „szintaktikai zaj” része.

Func<Customer, Order, CityOrder> cityOrderSelector =

    delegate(Customer c, Order o) { return new CityOrder(c.City, o.Amount); };

Ezt a fontos tényt a LINQ csapat már a projekt elején felismerte és hozzákezdett a C# nyelv elemeinek olyan bővítéseihez, amelyek segítenek ezt a zajt jelentős mértékben csökkenteni, illetve ahol lehetséges, teljesen eltüntetni.

Az Architektúra Fórumon (http://.architekturaforum.hu) korábban publikált ciksorozatomban összegyűjtöttem és részletesen bemutattam azokat a nyelvi újításokat, amelyek a C# nyelvben megjelentek. Ezeket egy összefoglaló cikkben is elhelyeztem, amelyek az alábbi linken érhetők el:

http://architekturaforum.hu/blogs/inovak/archive/2007/06/20/a-c-3-0-nyelvi-250-jdons-225-gai-a-teljes-v-225-ltozat.aspx

A fenti kódrészletet a C# 3.0 új nyelvi képességei közül a lambda kifejezések, bővítő metódusok és a lokális típusfeloldás segítségével egyszerűbb formára hozhatjuk[1]:

public static List<ReportItem> GetSalesInfo(List<Customer> customers,

  List<Order> orders)

{

  var cityOrders = customers.Join(orders, c => c.Id, o => o.CustomerId,

    (c, o) => new CityOrder(c.City, o.Amount));

  var cityGroup = cityOrders.GroupBy(o => o.City);

  var result = cityGroup.Select(g => new ReportItem(g.Key, g.Count(),

    g.Sum(o => o.Amount)));

  return new List<ReportItem>(result);

}

Na, ha ezt a kódot nézzük, már hihetőbben hangzik az, hogy „szép”. A fenti kód érdekessége, hogy tulajdonképpen ugyanarra az MSIL kódra fordul le, mint a korábbi „frusztráló” kódrészlet: a C# nyelvi újdonságai ténylegesencsak a szintaktikai zaj leszűrését végzik. Az itt lévő kódban az eljáráshívásokat láncolhatjuk, illetve még mindig vannak olyan „tartalékok”, amelyek a C# 3.0 nyelvi lehetőségeivel tudunk kezelhetünk, mint például a CityOrder típus kiváltása az ún. névtelen típusokkal:

public static List<ReportItem> GetSalesInfo(List<Customer> customers,

  List<Order> orders)

{

  return customers

    .Join(orders, c => c.Id, o => o.CustomerId, (c, o) => new { c.City, o.Amount } )

    .GroupBy(o => o.City)

    .Select(g => new ReportItem(g.Key, g.Count(), g.Sum(o => o.Amount)))

    .ToList();

}

Ez már nagyon SQL-szerűen néz ki! Ez a C# kód még mindig gyakorlatilag ugyanazt az MSIL kódot eredményezi, mint az előző két változat. Eljutottunk addig a pontig, ahová a LINQ projekt tervezői is eljutottak: nyelvi elemmé lehet tenni az SQL-szerű lekérdezéseket! És a nyelvbe ez a képesség valóban be is került[2]:

public static List<ReportItem> GetSalesInfo(List<Customer> customers,

  List<Order> orders)

{

  return

    (      from c in customers      join o in orders on c.Id equals o.CustomerId      group new { c.City, o.Amount } by c.City into co      select new ReportItem(co.Key, co.Count(), co.Sum(o => o.Amount))

    ).ToList();

}

Ha ezt a lekérdezést hasonlítjuk össze a fejezet elején lévő SQL lekérdezéssel, akkor már érezhetjük, hogy miben is rejlik a LINQ ereje.

Hol járunk most?

Eddig megtudtuk a LINQ-ről, hogy nem csak egyszerűen mozaikszó, tartalma is van:

Ø  A LINQ új .NET típusok könyvtáraként jelenik meg, amelyek jelentős része a System.Core.dll assemblyben található a System.Linq névtérben.

Ø  A LINQ a C# (és a VB.NET) fordítókba beépülve SQL-szerű szintaktika használatát teszi lehetővé, amely a háttérben a LINQ részét képező objektumok kezelését végző kódra fordul.

Eddig még csak a jéghegy csúcsát láttuk. A felszín alatt további érdekes dolgok rejlenek...



[1] Megjegyzés: Természetesen, a C# 3.0 képességeit egyelőre csak a LINQ CTP változatának letöltésével használhatjuk a Visual Studio 2005-ben, illetve az Orcas Beta 1-ben. Én az utóbbival készítettem a cikket, és mindenkit ennek használatára bátorítanék — észrevételem alapján a LINQ CTP-je egy régebbi változat, nem minden ott szereplő kód fordul az Orcasban!

[2] Bár én itt csak a C#-ról beszélek, de ezek a LINQ képességek a VB.NET következő változatában is helyet kaptak.

Hét év a .NET-ben: 7. rész - A keretrendszer felépítése

Most, hogy már áttekintettük, hogyan is működik a .NET futtatókörnyezete, nézzük meg a keretrendszer egészének felépítését! A .NET fejlesztői szemmel több logikai rétegből épül fel, amelyeket a következő ábra foglal össze:

1.  A .NET keretrendszer logikai rétegei

A Common Language Runtime

A .NET-étege, amely közvetlenül az alkalmazás végrehajtásáért felelő a .NET-terminológiában a Common Language Runtime nevet viseli, amelyet magyarul talán a „nyelvfüggetlen futtatórendszer” kifejezés ír a legjobban le. A rövidség kedvéért én a továbbiakban a CLR betűszót fogom használni. A CLR logikailag két fontos részből áll:

Ø  Az ún. Common Type System – CTS (magyarul egységes típusrendszer)azokat a „natív” .NET típusokat definiálja, amelyek az alapjai a CLR működésének. A típusok definícióján túl megszabja együttműködésüket (pl. a konverziókat) és a metaadatok közötti megjelenés formáját is.

Ø  A .NET-nyelvek nem feltétlenül támogatják a CTS definiálta összes típust. Az egységes nyelvi sepcifikáció, azaz a Common Language Specification – CLS olyan szabályokat definiál és a típusok egy olyan részhalmazát, amelyek segítségével a fordítás során keletkező assemblyket garantáltan bármelyik .NET nyelvben felhasználhatjuk.

Fizikailag a CLR legfontosabb része az mscoree.dll fájl. Akkor jut szerephez, amikor az assemblyre hivatkozunk. Az mscoree.dll gondoskodik arról, hogy megtatlálja az assemblyt, betöltse, az assembly metaadatainak segítségével megtalálja benne a keresett típust, lefordítsa a megfelelő metódusokhoz tartozó MSIL-kódot, stb.

A Base Class Library

Ahhoz, hogy a CLR és a Win32 API-k szolgáltatásait ésszerűen és hatékonyan lehessen használni, rengeteg alapobjektumra szükség van, amelye a .NET nyelvek által jól használható objektumok köntösébe csomagolja a funkciókat. Ezt a célt szolgálja a .NET alapobjektumainak könyvtára (Base Class Library), amely logikailag számtalan módon felosztható. A .NET logikai rétegeit bemutató ábra csak néhány fontos elemet ragad ki ezekből. Számtalan olyan osztály, illetve szolgáltatásterület van, amely ezen az ábrán nem szerepel.

Az alaposztályok könyvtárát sok különálló assembly alkotja. Közülük a legfontosabb az mscorlib.dll fájl, amely a leggyakrabban használt típusokat tartalmazza, illetve az általános programozási feladatok megoldása során hasznos funkciókat. Amikor egy .NET-t vagy komponenst készítünk, az mscorlib.dll assemblyt mindig használjuk.

A Base Class Library valódi szerepe az, hogy ún. infrastrukturális szolgáltatásokat kínál a fejlesztőknek, vagyis olyan szolgáltatásokat, amelyek segítségével az operációs rendszer funkcióit reprezentáló Win32 API felett egy könnyen, jól és biztonságosan használható felületet kapunk. Bármilyen alkalmazás-architektúrában is gondolkodunk, az egyes architektúrarétegek mindegyike biztosan fel fogja használni a Base Class Library szolgáltatásait.

A .NET architektúra szolgáltatásai

A .NET keretrendszer más az első változataitól kezdődően a fejlesztők számára nemcsak infrastrukturális szolgáltatásokat kínált, hanem komoly architekturális szolgáltatásokat. Ezek olyan komoly értékkel és tartalommal bíró elemei a .NET keretrendszernek, amelyek a többrétegű[1] elosztott alkalmazások egyes architekturális rétegeinek megvalósítását segítik. Felhasználásukkal úgy építhetünk robusztus és skálázható architektúrát, hogy közben a .NET-környezet által kínált egyszerűséget és rugalmasságot megtarthatjuk.

A szolgáltatások önmagukban olyan terjedelmesek, hogy mindegyikhez könyvtárnyi irodalom tartozik. Ebben a cikkben én csak egy rövid áttekintésre vállakozom, kiemelve a legfontosabbakat.

Ø  ASP.NET: Az ASP.NET a webes alkalmazásfejlesztés alapvető eszköze a .NET keretrendszeren belül, amely több jól elhatárolt architektúraréteg elemeinek, komponenseinek megvalósítását támogatja a felhasználói felület és felhasználói folyamatok rétegének kialakításától egészen a webszolgáltatásokra épülő üzleti homlokzatok kialakításáig. Maga az ASP.NET talán a .NET keretrendszeren belül az egyik legösszetettebb architekturális szolgáltatás.

Ø  WinForms: Minden vastag kliens alkalmazás készítése során szükség van dialógusok, űrlapok, üzenetek megjelenítésére, amelyek egyszerű és összetett felhasználói felületelemeket egyaránt tartalmazhatnak. A .NET a WinForms eszköztárat kánálja erre a célra. A WinForms objektumhierarchiája segítségével egyszerűen kezelhetjük a vastag kliens alkalmazásokhoz tartozó felületelemeket, egyszerű leszármazással létrehozhatunk új vizuális komponenseket.

Ø  Webszolgáltatások: A szoftvertechnológiák kialakulása során mindig fontos szerepet kaptak azok, amelyek a platformok és a rendszerek meglévő rendeltetéseinek integrálását célozták meg. A webszolgáltatások használata napjainkban a legfontosabb technológia rendszerek — akár platformok között átívelő — integrálására. Annak, hogy a technológia elfogadottá, illetve szabvánnyá vált — úgy gondolom — jelentős szerepe van a .NET-nek, amely ennek a technológiának a használatát nem csak elérhetővé, de egyúttal igen egyszerűen használhatóvá is tette.

Ø  ADO.NET: A Microsoft az elmúlt 15 évben rengeteg módon próbálta meg az adatelérést, mint infrastrukturális szolgáltatást megteremteni. Ennek a legletisztultabb formája az ADO.NET-ben jelenik meg, amely szabványos felületet adó relációs adatbázisok és egyéb strukturált adatok elérésére (ún. ADO.NET providereken keresztül) és az ott található adatok egységes, az adatforrástól független reprezentációjára (az ún. DataSet-ek segítségével). Az ADO.NET egyszerű és áttekinthető modelt kínál az adatok lekérdezésére és adatbázisokba való visszaírására egyaránt.

Ø  Workflow Foundation (WF): A .NET 3.0-ban megjelenő Workflow Foundation a munkafolyamatok definiálását, végrehajtását és kezelését segítő infrastrukturális szolgáltatások — mint a neve is mutatja — alapjait teszi le. Lehetőséget nyújt ún. szekvenciális (a folyamat tevékenységeit időben egymást követő elemek hálójaként írjuk le), illetve állapotgépalapú (a folyamatot egy véges állapotú, események által vezérelt gépként írjuk le) munkafolyamatok kezelésére. A kialakítása kapcsán egyaránt használható humán és automatikus folyamatok leírására. A WF nem munkafolyamat-motor, jelen állapotában sok olyan szolgáltatás hiányzik belőle, amelyet azok általában megvalósítanak.

Ø  Windows Communication Foundation (WCF): A .NET-ben a kezdetek óta ott vannak azok az infrastruktúra szolgáltatások (például .NET remoting, webszolgáltatások, üzenetsorkezelés), amelyek elosztott rendszerek komponensei között a kommunikációr biztosítják. Ezek közül egy adott feladat, rendszer kapcsán az implementáció során mindig ki kellett a fejlesztőnek választania azt, amely az adott feladathoz a legjobban illeszkedik. A .NET 3.0-ban megjelenő Windows Communication Foundation egységes megoldást ad az elosztott rendszerek közötti kommunikációra korábban használt szétágazó megoldásaira. Lehetővé teszi a kommunikációs csatorna viselkedésének és futásidejű tulajdonságainak kialakítását a rendszerek újrafordítása nélkül, pusztán konfigurációs változtatások segítségével.

Ø  Windows Presentation Foundation (WPF): A Vista tervezésekor a Microsoft újraértelmezte a számítógépek grafikus alrendszerének fogalmát és egy olyan megoldást alkotott, amely maximálisan ki tudja használni a grafikus processzorok képességeit. Ennek kapcsán született meg a Vistában megjelenő, illetve a .NET 3.0 részeként is elérhető Windows Presentation Foundation, amely a régi vastag kliens felhasználói felület koncepciót „kihajítva” egy teljesen új koncepciót és az erre illeszkedő fejlesztési modellt foglal magában. A WPF segítségével fizikailag is elválaszthatóvá válik a képernyőn lévő vezérlőelemek vizuális megjelenése, illetve viselkedése. Lehetővé válik, hogy a programozási tapasztalatokkal bíró fejlesztő csak a vezérlőelemek viselkedésével (funkcionalitásának biztosításával) foglalkozzon, a megjelenését pedig grafikus, illetve designer alakítsa ki.

Ø  CardSpace: A Microsoft már régóta dolgozik ún. „Identity Metasystem” koncepcióján, amely valójában a digitális azonosítók tárolásának, kezelésének egy platformja. Ennek a koncepciónak a része a .NET 3.0 részeként elérhető CardSpace, amely a digitális azonosítók biztonságos tárolására szolgál. Ma már mindegyikünk számtalan  digitális azonosítóval rendelkezik: gondoljunk csak arra, hány webhelyen is rendelkezünk regisztrációval. A CardSpace természetesen nem csak ezeknek az azonosítóknak a tárolását végzi, hanem egységes felületet is nyújt egy adott művelethez, tranzakcióhoz (pl. bejelenkezés egy adott webhelyen, azonosítás elektronikus vásárlás során) tartozó digitális azonosító kiválasztásához.

Ø  Entity Framework: A .NET 3.5 változatához kapcsolódó ADO.NET v3.0 jelentősen túl lép az adatkezelés korábban használt eljárásain. Az ADO.NET  korábbi változatai elsősorban a relációs adatbáziskezelők fölé emeltek egy olyan absztrakciós réteget, amely lehetővé tette, hogy az ott található adatokat a fizikai adatbáziskezelőtől független objektumokként (DataSet és társai) láthassuk, használhassuk. Az Entity Framework ezt az absztrakciót fogalmi (koncepcionális) szintre emeli. ORM[2] technológiák segítségével lehetővé teszi, hogy az adatbázisainkban ne egyszerűen táblákat és közöttük lévő relációkat lássunk, hanem olyan magsszintű entitásokat, amelyek jól illeszkednek alkalmazásaink fogalmi modelljéhez. A jelenlegi információk szerint az Entity Framework a Visual Studio 2008 után fog megjelenni, várhatóan 2008 első félévében.

Egy-egy új .NET változat megjelenése általában a keretrendszer mindegyik logikai rétegében újdonságokat, változtatásokat kínál. A .NET 1.1-es és 2.0-ás változata elsősorban az infrastruktúra szolgáltatásokat bővítette, finomította. A 2.0-ás változat A WinForms és ASP.NET technológiákat már valódi architekturális szolgáltatásokká tette. A .NET 3.0 gyakorlatilag „csak” architekturális változtatásokkal bővítette a keretrendszert. A jövöben megjelenő .NET keretrendszerek várhatóan szintén elsődlegesen újabb architekturális szolgáltatásokkal fogják bővíteni a fejlesztők eszköztárát.



[1] Szándékosan használom a „többrétegű” szót a népszerűbb „háromrétegű” helyett, mert véleményem szerint pontosabban fedi le azt a tartalmat, amelyet jelöl.

[2] ORM: Object-Relational Mapping

 

István Novák

No list items have been added yet.

Feed

The owner hasn't specified a feed for this module yet.
There are no photo albums.
Ebben a listában gyűjtöm össze azokat a témakat, amelyeket pulikálni tervezek. A lista ennek megfelelően folyamatosan változik.