Zbyt wiele razy spotkałem się z takim kodem:
1: private void Form1_Load(object sender, EventArgs e) 2: { 3: btnAddUser.Visible = Thread.CurrentPrincipal.IsInRole("can add user"); 4: btnShowPosts.Visible = Thread.CurrentPrincipal.IsInRole("can show posts"); 5: //... 6: //... 7: //... 8: //... 9: 10: }
Naprawdę, pisanie takiego kodu w czasach, gdy programowanie obiektowe nie jest “nowością” (czyli od dobrych kilkunastu lat) to po prostu grzech. Tym bardziej, że w bardziej rozbudowanych systemach powyższe instrukcje mogą składać się z SETEK podobnych linii. Kto zgadnie jak bardzo podatna na błędy programisty jest owa metoda?
Na szczęście można temu zaradzić, pewnie na milion różnych sposobów. Moja propozycja (dla WinForms, ale przy odrobinie chęci można to “zgeneralizować”) jest taka:
1) tworzymy atrybut, który, nałożony na klasę, daje możliwość zadeklarowania nazwy operacji oraz uprawnień wymaganych do jej wykonania:
1: [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 2: public sealed class UserOperationAttribute : Attribute 3: { 4: private readonly string[] _requiredPrivileges; 5: public readonly string DisplayName; 6: 7: public UserOperationAttribute(string displayName, params string[] requiredPrivileges) 8: { 9: _requiredPrivileges = requiredPrivileges; 10: DisplayName = displayName; 11: } 12: 13: public bool IsSatisfied 14: { 15: get 16: { 17: // current user has all required privileges 18: return _requiredPrivileges.All(Thread.CurrentPrincipal.IsInRole); 19: } 20: } 21: }
2) Specjalna klasa wyszukuje w załadowanych assemblies te typy, które mają ów atrybut zaaplikowany a następnie sprawdza, czy warunek wymagany do udostępnienia operacji użytkownikowi jest spełniony; efektem działania jest słownik zawierający typy będące operacjami oraz instancje przypisanych im atrybutów:
1: public class UserOperationDiscovery 2: { 3: public static Dictionary<Type, UserOperationAttribute> Perform() 4: { 5: var actions = from type in AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes()) 6: let actionAttribute = type.GetCustomAttributes(typeof(UserOperationAttribute), false) 7: .Cast<UserOperationAttribute>().SingleOrDefault() 8: where actionAttribute != null && actionAttribute.IsSatisfied 9: select new { type, attr = actionAttribute }; 10: 11: return actions.ToDictionary(x => x.type, x => x.attr); 12: } 13: }
3) Z założenia atrybut ten aplikowałem do własnych UserControls. W formie miałem dwa panele: jeden przeznaczony do wyświetlenia aktualnej operacji i jeden służący do prezentowania wszystkich operacji dostępnych dla użytkownika. Operacje uruchamiane były przyciskami (każdej wykrytej operacja tworzyłem przycisk), wciśnięcie przycisku powodowało utworzenie kontrolki i osadzenie jej w odpowiednim placeholderze:
1: private void LoadOperations() 2: { 3: var operations = UserOperationDiscovery.Perform(); 4: operations.ToList().ForEach( 5: o => 6: { 7: // new button for each operation 8: Button btn = new Button 9: { 10: Text = o.Value.DisplayName, 11: Dock = DockStyle.Fill, 12: Tag = o.Key 13: }; 14: 15: btn.Click += (s, e) => 16: { 17: // dispose of currently displayed operation 18: if (Placeholder.Controls.Count > 0) 19: Placeholder.Controls[0].Dispose(); 20: Placeholder.Controls.Clear(); 21: 22: // create new instance of operation on each click 23: var newControl = (Control)Activator.CreateInstance((Type)btn.Tag); 24: newControl.Dock = DockStyle.Fill; 25: Placeholder.Controls.Add(newControl); 26: }; 27: 28: this.buttonsPanel.Controls.Add(btn); 29: }); 30: }
Mechanizm ten jest bardzo prosty, dzięki czemu łatwo można go dostosować do własnych potrzeb (jak np. logiczne grupowanie operacji w dedykowanych miejscach interfejsu użytkownika). Dodanie nowego ekranu sprowadza się do utworzenia kontrolki i zaaplikowaniu jej atrybutu – reszta dzieje się sama. Zaprezentowany na początku posta syf idzie do kosza naprawdę małym nakładem pracy.