środa, 9 marca 2011

button.Enabled = true or not?


Z czasem można dostać zawrotu głowy od banalnych rzeczy. Szczególnie, gdy są one zależne miedzy sobą, mnożą się w nieskończoność i są ustawniane w wielu i, nierzadko, w niespodziewanych miejscach. Spotkać można się z taką sytuacją w przypadku sterowania dostępnością kontrolek. Mam na myśli ustawianie właściwości elementów interfejsu użytkownika, które określają, czy kontrolka jest włączona i/lub widoczna. Przykładowo chcemy umieścić przycisk, który pojawia się i znika w zależności od paru parametrów:
- użytkownik ma ograniczone prawa,
- dany obiekt nie może być edytowany, bo został zatwierdzony
- jest godzina 4.38, dane są arhiwizowane - nie można wprowadzać modyfikacji.

Fragment metody ustwiający przycisk mogłby wyglądać tak:

button.Enabled = Autoryzation.CurrentUser.Role == Roles.Admin

&& !this.CurrentItem.Sealed
&& DateTime.UtcNow.Hour != 4;

Sprawa jest jak do tej pory prosta, ale wprowadźmy małe utrudnienie.
Chcemy, aby wszystkie przyciski modyfikujące dane były wyłączone w sytuacji, gdy aplikacja czeka na asynhroniczną odpowiedź serwera. Nie chcemy wyłączać całych paneli i\lub interfejsu, ponieważ zależy nam na dostępności i na jak najlepszym samopoczuciu użytkownika. Oczekujemy rozwiązania, które załatwi sprawę za jednym zamachem w całej aplikacji, bez dodawnia takiej obsługi do każdego modułu/ekranu. Znajdujemy więc klasę, która zajmuje się odpytywaniem serwera i tworzymy w niej mechanizm chodzący po wszystkich widocznych w danej chwili elementach i dochodzimy do takiego miejsca:

foreach(Control control in controls)
{

control.Enabled = isProxyBusy;
}

Czy na pewno o to nam chodziło?
Mechanizm ewidentnie nadpisuje istniejącą już logikę i odkrywa przed użytkownikiem wcześniej niedostępne funkcje.
Oczywiście można dodać jakiś słownik, który będzie przechowywał stan właściwości Enabled przed jej przestawieniem. Niestety nie zadziała to w przypadku, gdy użytkownik w tym czasie, gdy proxy będzie zajęte, zaloguje się, czy zmieni zaznaczony obiekt. Mechanizm zapamiętywania przywróci właściwość Enabled do nieaktualnego już stanu.
Nawet, gdy to opanujemy mogą pojawić się inne problemy.

Wyobrażmy sobie, że po roku inny programista impementuje funkcjonalność, która pozwala administratorowi wyłączyć edycję w aplikacji ze względu na np zamknięcie miesiąca lub awarię:

private void TurnOffEdit()
{
...

foreach(Button button in buttons)
{
button.Enabled = false;
}
}

Uzyskaliśmy funkcjonalność, którą równie dobrze można uzyskać w taki o to sposób:

Random r = new Random();
foreach(Control ctrl in controls)
{

ctrl.Enabled = r.Next(1) == 0;
}


Rozwiązanie:

Piszemy semafor, który będzie przechowywał “zgłoszenia” włączania i wyłączania kontrolek oraz w odpowiednim momencie rozstrzygnie i ustawi odpowiednią właściwość kontolki. Użycie takiego semafora może wyglądać tak:

button.SetAcces("Użytkownik jest administratorem?", Autoryzation.CurrentUser.Role == Roles.Admin);
button.SetAcces("Proxy jest zajęte?", !this.CurrentItem.Sealed);


Pierwszy parametr jest tu identyfikatorem w postaci łańcuchu znaków, potrzebny semaforowi do grupowania zgłoszeń wg przyczyny. Druga zmienna określa, czy kontrolka ma być włączona.
I jeśli kolejny programista dopisze:

bool buttonOn = false;
button.SetAcces(“Admin ma dobry nastrój?”, buttonOn);


Problemem tego, czy przycisk ma być widoczny czy nie, zajmie się nasz semafor;)

Ponieważ chcemy, aby semafor był dostępny przy zmiennej, ale nie mamy dostępu do implementacji kontrolek, skorzystamy z metody rozszerzającej:

public static class ControlAccesSemaphore
{
public static void SetAcces(this Control control, string reason, bool value)
{
...
}
}

Semafor będzie przechowywał zgłoszenia w słowniku, którego kluczem będzie para: kontrolka i powód, dla którego jest włączona/wyłączona.

class ControlReason
{
private Control _control;
private string _reason;

public Control Control
{
get
{
return _control;
}
}

public string Reason
{
get
{
return _reason;
}
}

public ControlReason(Control control, string reason)
{
if (control == null || string.IsNullOrWhiteSpace(reason))
{
throw new ArgumentNullException();
}
_control = control;
_reason = reason;
}

public override bool Equals(object obj)
{
ControlReason ctrlNotyfy = obj as ControlReason;
if (ctrlNotyfy == null)
{
return false;
}
return _control.Equals(ctrlNotyfy.Control)
&& _reason.Equals(ctrlNotyfy.Reason);
}

public override int GetHashCode()
{
return _control.GetHashCode() ^ _reason.GetHashCode();
}

public override string ToString()
{
return string.Format("Control: {0}, reason: {1}", _control.ToString(), _reason);
}
}

Nadpisałem metody Equals oraz GetHashCode, aby można było prawidłowo używać tej klasy, jako klucz słownika.
Uzupełnijmy teraz metody w semaforze. SetAcces może wyglądać tak:

public static void SetAcces(this Control control, string reason, bool value)
{
ControlReason newControlReason = new ControlReason(control, reason);

if (_controlReasonValues.ContainsKey(newControlReason))
{
_controlReasonValues[newControlReason] = value;
}
else
{
_controlReasonValues.Add(newControlReason, value);
}

SetControlsProp();
}

Natomiast SetControlsProp, która ustawia właściwość Enabled, może wyglądać tak:

private static void SetControlsProp()
{
var groups =
from g in _controlReasonValues
group g by g.Key.Control into gg
select new
{
Key = gg.Key,
Value = gg.All(a => a.Value)
};

foreach (var controlGroup in groups)
{
controlGroup.Key.IsEnabled = controlGroup.Value;
}
}
Są tutaj grupowane wszystkie zgłoszenia wg kontrolki i jeśli wszystkie zgłoszenia mają wartość true, to tylko w tym przypadku kontrolkę można włączyć.

Kod źródłowy

Brak komentarzy:

Prześlij komentarz