UsersDataSource a RolesDataSource - deklarativní přístup k membership datům

Za podstatnou nevýhodu membership modelu v ASP.NET považuji to, že nedává žádný prostředek, jak deklarativně pracovat s uživateli. Není možno si jednoduše vypsat třeba seznam uživatelů nebo rolí.

Tento problém ale má řešení: napsat si vlastní DataSource. DataSource je nevizuální serverový ovládací prvek, který slouží jako univerzální rozhraní, umožňující bindovaným prvkům přistupovat k jakémukoliv datovému zdroji. Součástí .NET Frameworku jsou controly pro přístup k obecným datovým zdrojům (typicky SQL databáze), ale nic vám nebrání v tom, napsat si vlastní.

Podrobnější informace o deklarativním data bindingu je jeho použití najdete mimo jiné i ve videoarchivu:

Výsledkem mého snažení budou dva controly. RolesDataSource a UsersDataSource. První jmenovaný bude vracet seznam rolí a počet uživatelů v nich, druhý všechny standardně dostupné údaje o uživatelích. Obojí využívá standardní strukturu membership providerů. Prvky tedy budou fungovat bez ohledu na to, jaký konkrétní provider bude použit.

Prvky jsou implementované jako "read only", lze pomocí nich data pouze zobrazovat, nikoliv měnit. Deklarativní přístup k modifikacím dat tohoto typu mi nepřijde příliš šťastný. Nezabýval jsem se zatím ani parametrizací dotazů, i když u uživatelů to do budoucna plánuji.

RolesDataSource

Vývoj komponenty si popíšeme na prvku RolesDataSource:

using System;

using System.Web;

using System.Web.Security;

using System.Web.UI;

using System.Web.UI.Design;

 

namespace Altairis.Web.UI.DeclarativeMembership {

 

    [ToolboxData("<{0}:RolesDataSource runat=server />")]

    //[System.ComponentModel.Designer(typeof(RolesDataSourceDesigner))]

    public class RolesDataSource : DataSourceControl {

        private RolesView roles;

 

        protected override DataSourceView GetView(string viewName) {

            if (!string.IsNullOrEmpty(viewName) && !viewName.Equals("MembershipRoles", StringComparison.OrdinalIgnoreCase)) throw new ArgumentOutOfRangeException("viewName", viewName, "Specified view does not exists.");

            if (this.roles == null) this.roles = new RolesView(this, viewName);

            return this.roles;

        }

 

        protected override System.Collections.ICollection GetViewNames() {

            return new string[] { "MembershipRoles" };

        }

 

        private sealed class RolesView : DataSourceView {

 

            public RolesView(RolesDataSource owner, string ViewName) : base(owner, ViewName) { }

 

            protected override System.Collections.IEnumerable ExecuteSelect(DataSourceSelectArguments arguments) {

                arguments.RaiseUnsupportedCapabilitiesError(this);

 

                string[] roles = Roles.GetAllRoles();

                Role[] r = new Role[roles.Length];

                for (int i = 0; i < roles.Length; i++) r[i] = new Role(roles[i], Roles.GetUsersInRole(roles[i]).Length);

                return r;

            }

 

        }

    }

 

    public sealed class Role {

 

        public Role(string name, int users) {

            this.roleName = name;

            this.userCount = users;

        }

 

        private string roleName;

 

        public string RoleName {

            get { return roleName; }

            set { roleName = value; }

        }

 

        private int userCount;

 

        public int UserCount {

            get { return userCount; }

            set { userCount = value; }

        }

 

    }

 

}

Control sám je třída, odvozená od base class System.Web.UI.DataSourceControl. Implementuje dvě pro nás zajímavé metody:

  • GetViewNames vrací seznam názvů podporovaných pohledů na data. V našem případě bude takový pohled pouze jeden.
  • GetView pak vrací vlastní data. Naše implementace je velmi jednoduchá, stará se pouze o jistou formu cacheování, aby se data nenačítala z providera neustále dokola.

Vlastní práci odvede třída RolesView, která je poděděná od System.Web.UI.DataSourceView a reprezentuje vlastní data. My zde implementujeme pouze metodu ExecuteSelect, která vrátí data. Obdobně existují i metody ExecuteInsert, ExecuteUpdate a ExecuteDelete, ty ale náš prvek nepodporuje.

Metoda ExecuteSelect vrací kolekci (resp. IEnumerable) objektů, které představují vlastní položky. Vlastnosti objektů jsou pak použity jako hodnoty při data bindingu.

V našem případě se jedná o třídu Role, která je význačná především tím, že nic neumí, je to jenom kontajner na dvě vlastnosti RoleName a UserCount.

Design-time support

Výše uvedený control je plně funkční. Přidáte-li jej do stránky, vrátí vám seznam rolí a počet uživatelů z nich. Z hlediska vývojáře ale postrádá jistý komfort, na který jsme u prvků tohoto typu zvyklí. Například si neumí zjistit schéma vrácených dat a podle toho upravit výchozí šablonu navázaných prvků. Musíte se tedy spolehnout na runtime autogenerování nebo vědět, že máte na vhodné místo napsat <%# Eval("RoleName") %>, což není právě pohodlné.

Naštěstí, náprava je snadná. Stačí k prvku vytvořit takzvaný Designer. To je třída, kterou si volá vývojové prostředí (tedy typicky Visual Studio) při práci s daným controlem v GUI. Designer je jakási potěmkinova vesnice, třída, která dělá "jako" totéž, co vlastní control. Nevrací ale (obvykle) reálná data, jenom jejich strukturu a jakési demo.

public class RolesDataSourceDesigner : DataSourceDesigner {

    private RolesView roles;

 

    public override DesignerDataSourceView GetView(string viewName) {

        if (!string.IsNullOrEmpty(viewName) && !viewName.Equals("MembershipRoles", StringComparison.OrdinalIgnoreCase)) throw new ArgumentOutOfRangeException("viewName", viewName, "Specified view does not exists.");

        if (this.roles == null) this.roles = new RolesView(this, viewName);

        return this.roles;

    }

 

    public override string[] GetViewNames() {

        return new string[] { "MembershipRoles" };

    }

 

    private sealed class RolesView : DesignerDataSourceView {

 

        public RolesView(RolesDataSourceDesigner owner, string ViewName) : base(owner, ViewName) { }

 

        public override IDataSourceViewSchema Schema {

            get {

                TypeSchema ts = new TypeSchema(typeof(Role));

                return ts.GetViews()[0];

            }

        }

 

        public override System.Collections.IEnumerable GetDesignTimeData(int minimumRows, out bool isSampleData) {

            if (minimumRows < 5) minimumRows = 5;

            isSampleData = true;

            Role[] r = new Role[minimumRows];

            for (int i = 1; i <= minimumRows; i++) r[i - 1] = new Role("Role #" + i, i);

            return r;

        }

 

    }

 

}

Designerem je třída RolesDataSourceDesigner, poděděná od System.Web.UI.Design.DataSourceDesigner. Namespace System.Web.UI.Design se nachází v assembly System.Design, kterou si musíte nareferencovat.

Rozhraní designéru je velmi podobné rozhraní opravdového prvku, pouze vrací ukázková data vycucaná z prstu. Pro nás je zajímavá zejména vlastnost Schema, protože ta vrací popis struktury používaných dat. Využívám zde pomocnou tříduTypeSchema, která automaticky vygeneruje potřebné přes reflection. Pokud chcete mít nad výsledkem větší kontrolu, doporučuji vaší laskavé pozornosti článek linkovaný níže.

Důležitým krokem je také sdělit systému, že pro control má použít vámi určený designer. To se dělá pomocí atributu System.ComponentModel.Designer. V prvním výpisu je již přítomen, stačí příslušný řádek odkomentovat.

Po rekompilaci, kdykoliv přiřadíte bindovaný prvek ke svému DataSource, naplní se výchozí šablona podle vámi definované struktury. Upozorňuji, že testovací ASPX stránku je po rekompilaci dobré zavřít a znovu otevřít v editoru, jinak se změna nemusí projevit hned.

UsersDataSource

Prvek UsersDataSource, který vrací uživatele, je sice o něco komplikovanější, ale pouze z hlediska složitější struktury vráceného objektu. Na vývoj je dokonce o maličký kousek jednodušší, protože nemusíme vytvářet vlastní třídu, ale můžeme použít vestavěnou System.Web.Security.MembershipUser.

Závěr

Prvky DataSource umožňují deklarativní práci doslova s jakýmkoliv zdrojem dat a jejich implementace není příliš složitá. Příkladem může být třeba RSS toolkit, který umožňuje tímto způsobem přistupovat k RSS feedům.

Zdrojové kódy obou controlů si můžete stáhnout zde: 20070202-DeclarativeMembership.zip

Při tvorbě tohoto článku (a controls :-) jsem používal následující články v MSDN:

Popisují tvorbu data source controls, a to do mnohem větší hloubky, než já.

  • Altairis
  • Nemesis
  • Microsoft MVP
  • IIS
  • ASP.NET