niedziela, 9 maja 2010

Menu użytkownika w pliku xml cz.1 - Podstawa

Pisząc aplikację w ASP.NET MVC na pewno będziemy borykać się z problemem sposobu przechowywania głównego menu jakie powinno zostać wyświetlone użytkownikowi. Jeżeli opieramy działanie na uniwersalnym menu to oczywiście najlepiej wpisać je w master page i zapomnieć o całej sprawie. Najczęściej jednak użytkownicy mają różne prawa/role, co często ciągnie za sobą brak autoryzacji do pewnych funkcjonalności. Wylądowaliśmy zatem w sytuacji gdzie w zależności praw dostępu użytkownika powinniśmy wygenerować mu inne menu.

Możliwe rozwiązania

Do dyspozycji mamy zatem następujące możliwości zapisania menu:
  • na stałe w kodzie
  • w pliku
  • w bazie danych
Zapis na stałe w kodzie jest oczywiście z definicji zły dla tego typu dynamicznych elementów. Kolejne dwa rozwiązania są już znacznie lepsze (choćby dlatego, że odpada potrzeba ponownej kompilacji i możemy dowolnie edytować informacje). Warto jednak zwrócić uwagę, że do obsługi menu niekoniecznie trzeba używać bazy danych (dużą rolę odgrywa tutaj wielkość aplikacji). Jeżeli dysponujemy stałym zestawem ról dla użytkowników można swobodnie do tego celu wykorzystać plik XML.

Docelowa struktura

Odpowiednik menu użytkownika w xml powinien zawierać elementy:
  • typ/prawa/rolę użytkownika
  • adres docelowy
  • opis wyświetlanego linku
W związku z tym wybrałem następującą strukturę zapewniającą powyższe założenia:
<!--menu.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<menus>
  <menu for="Doctor">
    <item value="Dzisiejsze wizyty" url="/Doctor/Index" />
    <item value="Ostatnia wizyta" url="/Visit/Last" />
    <item value="Wyloguj" url="/User/Logout" />
  </menu>
  <menu for="Recepcionist">
    <item value="Nowy pacjent" url="/Patient/Create" />
    <item value="Wyloguj" url="/User/Logout" />
  </menu>
</menus>
Jak widać w przykładzie mamy dwa osobne menu. Jedno dla lekarza z trzema wpisami i drugie dla recepcjonisty z dwoma. Struktura jest przy okazji na tyle prosta, że bardzo łatwo da się ją odzwierciedlić w kodzie. A jak już o tym mowa, najpierw nieco abstrakcji:
[Flags]
public enum UserRole
{
    Doctor = 1 << 0,
    Recepcionist = 1 << 1,
}

public interface IMenu
{
    IEnumerable<IMenuItem> For(UserRole role);
}

public interface IMenuItem
{
    string Value { get; }
    string Url { get; }
}
Interfejs IMenu reprezentuje już wczytane menu dając dając dostęp do listy elementów IMenuItem czyli informacji opisujących link jaki później zostanie wygenerowany. Dodajmy zatem do tego wszystkiego obsługę xml i jesteśmy w domu.
[XmlRoot("menus")]
public class XmlMenu : IMenu
{
    [XmlElement("menu")]
    public List<XmlRoleMenu> RoleMenus { get; set; }

    public XmlMenu()
    {
        RoleMenus = new List<XmlRoleMenu>();
    }

    public IEnumerable<IMenuItem> For(UserRole role)
    {
        XmlRoleMenu roleMenu = RoleMenus
            .Where(m => m.Role == role)
            .Single();
        return roleMenu.Items;
    }

    public static IMenu LoadFrom(string filePath)
    {
        XmlSerializer serializer = new XmlSerializer(typeof(XmlMenu));
        XmlMenu menu = null;
        using (StreamReader reader = new StreamReader(filePath)) {
            menu = (XmlMenu)serializer.Deserialize(reader);
            reader.Close();
        }
        return menu;
    }
}

public class XmlRoleMenu
{
    [XmlElement("item")]
    public List<XmlMenuItem> MenuItems { get; set; }

    [XmlAttribute("for")]
    public UserRole Role { get; set; }

    public XmlRoleMenu()
    {
        MenuItems = new List<XmlMenuItem>();
    }

    public IEnumerable<IMenuItem> Items
    {
        get { return MenuItems.ConvertAll(item => (IMenuItem)item); }
    }
}

public class XmlMenuItem : IMenuItem
{
    [XmlAttribute("value")]
    public string Value { get; set; }
    [XmlAttribute("url")]
    public string Url { get; set; }
}
Na zakończenie jeszcze przykład użycia wyciągający menu dla doktora
static void Main(string[] args)
{
    IMenu menu = XmlMenu.LoadFrom("menu.xml");
    foreach (IMenuItem item in menu.For(UserRole.Doctor)) {
        Console.WriteLine("{0}\t{1}", item.Url, item.Value);
    }
    Console.ReadLine();
}
Prawda, że proste? Wczytywanie pliku to jednak nie wszystko czego potrzebujemy. Takie menu należy jeszcze osadzić w widoku asp.net mvc, ale o tym już niedługo.

5 komentarzy:

  1. Najs ;) Kto by pomyslal kogo spotkalem w moim RSS feed :>

    OdpowiedzUsuń
  2. Patrz, a my głupi PHPowcy używamy do tego partiali, a ci zwyrodnialcy od Pythona dziedziczenia szablonów

    OdpowiedzUsuń
  3. @Up
    W asp można co prawda tworzyć master.page na podstawie innych i w ten sposób zbudować menu. Ale prowadzi to do rozgałęzienia, gdzie późniejsze ewentualne zmiany trzeba wykonywać na każdym dziecku. Do zrobienia, tylko czy jest to wygodne.

    OdpowiedzUsuń
  4. Dlaczego wartości UserRole są definiowane z przesunięciem bitów?

    btw, ciekawy artykuł :)

    OdpowiedzUsuń
  5. @Up
    Tak się składa, że w tym przypadku przesunięcia nie odgrywają żadnej roli (przynajmniej na razie). Jest to spadek z jednej aplikacji gdzie do autoryzowania akcji w mvc używam niestandardowego filtru dzięki któremu zamiast:
    [Authorize(Roles="Doctor,Recepcionist"]
    Mam
    [AuthorizeFor(Role=UserRole.Doctor | UserRole.Recepcionist)]

    OdpowiedzUsuń