wtorek, 15 marca 2011

Rozszerzanie stringów

Spójrzmy na tego kulfona:

if( cloneableMetadataConcatenatorStr == “option1”
|| cloneableMetadataConcatenatorStr == “option2”
|| cloneableMetadataConcatenatorStr == “option3”
|| cloneableMetadataConcatenatorStr == “option4”)
{

}

I porównajmy do tego:

if( cloneableMetadataConcatenatorStr.In(“option1”, “option2”, 
“option3”, “option4”)
{

}

Oba fragmenty robią to samo, ale który z nich daje ci więcej natchnienia?

Metoda In() w string niestety nie istnieje, ale możemy ją łatwo dokleić. Wystarczy, że napiszemy klasę zawierającą metodę rozszerzającą string’a:

namespace Helpers
{
public class ExttentionMethods
{
public static In(this string obj, params string[] options)
{
return options.Contains(obj);
}
}
}

Sposobów na wykorzystanie tej funkcjonalności jest bardzo wiele.
Co prawda, nie będziemy chcieli rozszerzać klasy za każdym razem, gdy znajdziemy ku temu okazję, ale w często występujących przypadkach pomoże nam to zaoszczędzić czasu, ilości pisanego kodu oraz doda do kodu przejrzystości i świeżości.
Z pewnością nie ominie nas w aplikacji logowanie błędów, czy walidowanie parametrów metod:

catch (Exception e)
{
ErrorLogger.Log(e);
}

możemy zastąpić:

catch (Exception e)
{
e.Log();
}

oraz:

void SomeMethod(object mustBeNotNull)
{
if (mustBeNotNull == null)
{
throw new ArgumetNullException(mustBeNotNull);
}
}

zastępujemy:

void SomeMethod(object mustBeNotNull)
{
mustBeNotNull.ThrowIfNull(“mustBeNotNull”);
}

poprzez napisanie wcześniej paru linijek:

public static ThrowIfNull(this object obj, string objName)
{
if( obj == null )
{
throw new ArgumetNullException(objName);
}
}

Użycie metody rozszerzającej tak naprawdę jest tłumaczone przez kompilator na wywołanie metody statycznej, której pierwszym parametrem jest obiekt, na którym wywoływana jest metoda. Z tego powodu, jeśli obiekt będzie null’em to nie zostanie rzucony wyjątek tak jak przy wywołaniu metody instancji.

Kolejne przykłady użycia:

bool isHour = 17.Between(0, 24);
int number = “158”.ToInt();
DataTable dt = “SELECT * FROM WORKERS”.Execute();

ś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