Gadu gadu Skype

Archiwum dla tagu: ‘Programowanie’

lis
20

Visual State Manager

Spook, Listopad 20 2011

Napisz komentarz

Kiedy piszemy aplikację okienkową, prędzej czy później dochodzi do sytuacji, w której trzeba powiązać interfejs użytkownika z pewnymi metodami w kodzie. W przypadku prostych programików problem w zasadzie nie istnieje, ale gdy aplikacja rozrasta się, sytuacja staje się nieco bardziej skomplikowana.

Przyjmijmy, że pojedynczą funkcjonalność wywoływaną z poziomu interfejsu użytkownika nazwiemy akcją. Podstawowym problemem jest fakt, iż do akcji można dostać się z wielu różnych miejsc w interfejsie użytkownika. Na przykład może być ona dostępna równolegle z poziomu menu aplikacji, paska narzędzi oraz menu kontekstowego. W takiej sytuacji nie ma już mowy o oprogramowywaniu zdarzeń elementów interfejsu (w sensie: implementowania akcji wewnątrz metody obsługi zdarzenia), jak można byłoby zrobić w prostej aplikacji, ponieważ utrzymywanie takiego kodu będzie horrorem. Rozwiązanie tego problemu nie jest zbyt trudne – wystarczy samą funkcjonalność przenieść do osobnej metody i wywoływać ją w metodach obsługujących zdarzenia.

To jednak nie wszystko. Często zdarza się, że pewną akcję można wywołać tylko w odpowiednich warunkach. Na przykład edycja i usunięcie elementu znajdującego się na liście jest możliwa tylko wówczas, gdy element ten jest zaznaczony. Oczywiście w odpowiednich metodach można sprawdzać zawsze, czy wszystkie konieczne warunki są spełnione, ale takie rozwiązanie jest niewygodne z punktu widzenia użytkownika. Znacznie mniej frustrująca jest sytuacja, w której jasno widać, że akcja jest niedostępna, niż gdy wprawdzie można ją wywołać, ale tylko po to, aby dostać po oczach komunikatem ostrzegawczym (lub, o zgrozo, bez żadnego efektu).

Notepad - zawijanie wierszy

Spośród akcji można wyróżnić też takie, których zadaniem jest przełączenie pewnego stanu. Przykładem może być zawijanie wierszy w Notatniku: haczyk obok odpowiedniej pozycji menu informuje o tym, czy dana akcja jest aktywna, czy nie.

Ustawienie własności Enabled danej kontrolki w sytuacji, gdy zmiana warunków powoduje uniemożliwienie wywołania odpowiadającej jej akcji nie jest problemem. Gorzej, gdy po pierwsze – jak już wspominałem – akcja jest wywoływana z poziomu kilku kontrolek, a po drugie – warunków wymaganych do wywołania akcji jest kilka. Zauważmy też, że przeciętna aplikacja zawiera tych akcji kilkanaście lub kilkadziesiąt. Próba uzmysłowienia sobie, ile warunków w różnych miejscach trzeba byłoby wprowadzić, aby ręcznie aktualizować stany akcji może przyprawić o ból głowy.

Dużo frameworków wprowadza gotowe rozwiązania. Delphi udostępnia użytkownikom niewizualny komponent TActionManager, w którym można definiować akcje oraz odpowiadające im kontrolki. Windows Presentation Foundation oferuje podobny, bardzo wygodny w użyciu mechanizm. Ja chciałbym zaprezentować prostego managera przydatnego w sytuacji, gdy framework nie udostępnia nam odpowiedniego gotowca lub gdy nie chcemy korzystać z dużego rozwiązania, być może znacznie przerastającego nasze potrzeby.

Mój pomysł opiera się na trzech elementach: State, Condition i Action. Rozpatrzmy je po kolei.

State

Obiekt State jest odpowiedzialny za przechowywanie informacji o pojedynczym atomowym stanie jakiegoś elementu aplikacji. Na przykład może to być informacja o tym, czy w liście został zaznaczony jakiś element lub czy użytkownik zaznaczył jakiś tekst w polu tekstowym. Stan musi być użyty w taki sposób, by można było łatwo i jednoznacznie określić jego wartość, a także by możliwie łatwo można było go aktualizować. Na przykład kontrolka listy elementów udostępnia zdarzenie informujące o tym, że zmienił się indeks zaznaczonego elementu, zaś pole tekstowe – również poprzez zdarzenie – informuje o zmianie zaznaczenia. W obsłudze tych zdarzeń można łatwo określić wartość odpowiadającego im stanu.

Celem istnienia tej klasy jest ujednolicenie dostępu do warunków mogących wpływać na stany akcji.

Oto przykładowa implementacja klasy stanu w C#:

public class StateChangedEventArgs : EventArgs
{
  private bool newValue;

  public StateChangedEventArgs()
  {
    newValue = true;
  }

  public StateChangedEventArgs(bool newNewValue)
  {
    newValue = newNewValue;
  }

  public bool NewValue
  {
    get
    {
      return newValue;
    }
    set
    {
      newValue = value;
    }
  }
}

public delegate void StateChangedDelegate(
  object State, 
  StateChangedEventArgs e);

public class State
{
  private bool value;

  public State(bool initialValue = true)
  {
    value = initialValue;
    StateChanged = null;
  }

  public bool Value
  {
    get
    {
      return value;
    }
    set
    {
      this.value = value;
      if (StateChanged != null)
        StateChanged(this, new StateChangedEventArgs(this.value));
    }
  }

  public event StateChangedDelegate StateChanged;
}

Condition

Condition jest klasą, która agreguje kilka różnych stanów. Dzięki temu jeden stan może być wykorzystywany w kilku różnych warunkach (być może w połączeniu z różnymi innymi stanami). Przykładowa implementacja w C# jest następująca:

public class ConditionChangedEventArgs : EventArgs
{
    private bool newValue;

    public ConditionChangedEventArgs()
    {
        newValue = true;
    }

    public ConditionChangedEventArgs(bool newNewValue)
    {
        newValue = newNewValue;
    }

    public bool NewValue
    {
        get
        {
            return newValue;
        }
        set
        {
            newValue = value;
        }
    }
}

public delegate void ConditionChangedDelegate(
  object Condition, 
  ConditionChangedEventArgs e);

public enum ConditionKind
{
    And,
    Or
}

public class Condition
{
    private List<State> stateList;
    private ConditionKind conditionKind;
    private bool value;

    private void StateChanged(object State, StateChangedEventArgs e)
    {
        EvaluateCondition();
    }

    private void EvaluateCondition()
    {
        if (stateList.Count == 0)
        {
            if (value != false)
            {
                value = false;

                if (ConditionChanged != null)
                    ConditionChanged(this, 
                      new ConditionChangedEventArgs(false));
            }
        }
        else
        {
            switch (conditionKind)
            {
                case ConditionKind.And:
                    {
                        bool tmpValue = true;
                        int i = 0;
                        while (i < stateList.Count && tmpValue)
                        {
                            tmpValue &= stateList[i].Value;
                            i++;
                        }

                        if (value != tmpValue)
                        {
                            value = tmpValue;

                            if (ConditionChanged != null)
                                ConditionChanged(this, 
                                  new ConditionChangedEventArgs(tmpValue));
                        }

                        break;
                    }
                case ConditionKind.Or:
                    {
                        bool tmpValue = false;
                        int i = 0;
                        while (i < stateList.Count && !tmpValue)
                        {
                            tmpValue |= stateList[i].Value;
                            i++;
                        }

                        if (value != tmpValue)
                        {
                            value = tmpValue;

                            if (ConditionChanged != null)
                                ConditionChanged(this, 
                                  new ConditionChangedEventArgs(tmpValue));
                        }

                        break;
                    }
            }
        }
    }

    public Condition()
    {
        stateList = new List<State>();
        value = false;
        conditionKind = ConditionKind.And;
    }

    public void AddState(State state)
    {
        int i = 0;
        while (i < stateList.Count && stateList[i] != state)
            i++;

        if (i < stateList.Count)
            return;

        state.StateChanged += StateChanged;
        stateList.Add(state);

        EvaluateCondition();
    }

    public void RemoveState(State state)
    {
        int i = 0;
        while (i < stateList.Count && stateList[i] != state)
            i++;

        if (i == stateList.Count)
            return;

        state.StateChanged -= StateChanged;
        stateList.RemoveAt(i);

        EvaluateCondition();
    }

    public ConditionKind ConditionKind
    {
        get
        {
            return conditionKind;
        }
        set
        {
            conditionKind = value;
            EvaluateCondition();
        }
    }

    public bool Value
    {
        get
        {
            return value;
        }
    }

    public event ConditionChangedDelegate ConditionChanged;
}

Action

Klasa Action opisuje pewną akcję. Powinna ona mieć następujące cechy:

  • Po powiązaniu z kontrolkami, powinna automatycznie przywiązać się do odpowiednich zdarzeń typu Click.
  • Powinna mieć własności typu Condition, regulujące dostępność (Enabled), widoczność (Visibility) oraz zaznaczenie (Down, Checked itp.) kontrolek, które są powiązane z akcją.
  • Powinna mieć zdarzenie Executed, wywoływane w momencie wybrania którejkolwiek z kontrolek, za które odpowiada akcja.

Oto przykładowa implementacja klasy Action:

public delegate void ActionExecutedDelegate(
  object Action, 
  EventArgs e);

public class Action
{
    private Condition enabledCondition;
    private Condition visibleCondition;
    private Condition checkedCondition;
    private List<object> controlList;

    private void ApplyEnabled(int i, bool newValue)
    {
        if (controlList[i] is Button)
            ((Button)controlList[i]).Enabled = newValue;
        else if (controlList[i] is ToolStripMenuItem)
            ((ToolStripMenuItem)controlList[i]).Enabled = newValue;
        else if (controlList[i] is ToolStripButton)
            ((ToolStripButton)controlList[i]).Enabled = newValue;
        else
            throw new InvalidOperationException(
              "Internal control list contains not supported control!");
    }

    private void ApplyEnabledToAll(bool newValue)
    {
        for (int i = 0; i < controlList.Count; i++)
            ApplyEnabled(i, newValue);
    }

    private void ApplyVisible(int i, bool newValue)
    {
        if (controlList[i] is Button)
            ((Button)controlList[i]).Visible = newValue;
        else if (controlList[i] is ToolStripMenuItem)
            ((ToolStripMenuItem)controlList[i]).Visible = newValue;
        else if (controlList[i] is ToolStripButton)
            ((ToolStripButton)controlList[i]).Visible = newValue;
        else
            throw new InvalidOperationException(
              "Internal control list contains not supported control!");
    }

    private void ApplyVisibleToAll(bool newValue)
    {
        for (int i = 0; i < controlList.Count; i++)
            ApplyVisible(i, newValue);
    }

    private void ApplyChecked(int i, bool newValue)
    {
        if (controlList[i] is Button)
            return;
        else if (controlList[i] is ToolStripMenuItem)
            ((ToolStripMenuItem)controlList[i]).Checked = newValue;
        else if (controlList[i] is ToolStripButton)
            ((ToolStripButton)controlList[i]).Checked = newValue;
        else
            throw new InvalidOperationException(
              "Internal control list contains not supported control!");
    }

    private void ApplyCheckedToAll(bool newValue)
    {
        for (int i = 0; i < controlList.Count; i++)
            ApplyChecked(i, newValue);
    }

    private void EnabledConditionChanged(object Condition, 
      ConditionChangedEventArgs e)
    {
        ApplyEnabledToAll(e.NewValue);
    }

    private void VisibleConditionChanged(object Condition, 
      ConditionChangedEventArgs e)
    {
        ApplyVisibleToAll(e.NewValue);
    }

    private void CheckedConditionChanged(object Condition, 
      ConditionChangedEventArgs e)
    {
        ApplyCheckedToAll(e.NewValue);
    }

    private void ToolStripMenuItemClicked(object sender, 
      EventArgs e)
    {
      if (ActionExecuted != null)
            ActionExecuted(this, new EventArgs());
    }

    private void ButtonClicked(object sender, EventArgs e)
    {
      if (ActionExecuted != null)
            ActionExecuted(this, new EventArgs());
    }

    void ToolStripButonClicked(object sender, EventArgs e)
    {
        if (ActionExecuted != null)
            ActionExecuted(this, new EventArgs());
    }

    public Action()
    {
        controlList = new List<object>();
        ActionExecuted = null;
        enabledCondition = null;
        visibleCondition = null;
        checkedCondition = null;
    }

    public void AddControl(object control)
    {
        int i = 0;
        while (i < controlList.Count && controlList[i] != control)
            i++;

        if (i < controlList.Count)
            return;

        if (control is Button)
            ((Button)control).Click += ButtonClicked;
        else if (control is ToolStripMenuItem)
            ((ToolStripMenuItem)control).Click += 
              ToolStripMenuItemClicked;
        else if (control is ToolStripButton)
            ((ToolStripButton)control).Click += 
              ToolStripButonClicked;
        else
            throw new ArgumentException("control", 
              "This control is not supported!");

        controlList.Add(control);

        if (enabledCondition != null)
            ApplyEnabled(controlList.Count - 1, 
              enabledCondition.Value);
        if (visibleCondition != null)
            ApplyVisible(controlList.Count - 1, 
              visibleCondition.Value);
        if (checkedCondition != null)
            ApplyChecked(controlList.Count - 1, 
              checkedCondition.Value);
    }

    public void RemoveControl(object control)
    {
        int i = 0;
        while (i < controlList.Count && 
          controlList[i] != control)
            i++;

        if (i == controlList.Count)
            return;

        if (control is Button)
            ((Button)control).Click -= ButtonClicked;
        else if (control is ToolStripMenuItem)
            ((ToolStripMenuItem)control).Click -= 
              ToolStripMenuItemClicked;
        else if (control is ToolStripButton)
            ((ToolStripButton)control).Click -= 
              ToolStripButonClicked;
        else
            throw new ArgumentException("control", 
              "This control is not supported!");
    }

    public void Execute()
    {
        if ((enabledCondition != null && 
            enabledCondition.Value == true) || 
          enabledCondition == null)
        {
            if (ActionExecuted != null)
                ActionExecuted(this, new EventArgs());
        }
    }

    public Condition EnabledCondition
    {
        get
        {
            return enabledCondition;
        }
        set
        {
            if (enabledCondition != null)
                enabledCondition.ConditionChanged -= 
                  EnabledConditionChanged;

            enabledCondition = value;

            if (enabledCondition != null)
            {
                enabledCondition.ConditionChanged += 
                  EnabledConditionChanged;

                ApplyEnabledToAll(enabledCondition.Value);
            }
        }
    }

    public Condition VisibleCondition
    {
        get
        {
            return visibleCondition;
        }
        set
        {
            if (visibleCondition != null)
                visibleCondition.ConditionChanged -= 
                  VisibleConditionChanged;

            visibleCondition = value;

            if (visibleCondition != null)
            {
                visibleCondition.ConditionChanged += 
                  VisibleConditionChanged;

                ApplyVisibleToAll(visibleCondition.Value);
            }
        }
    }

    public Condition CheckedCondition
    {
        get
        {
            return checkedCondition;
        }
        set
        {
            if (checkedCondition != null)
                checkedCondition.ConditionChanged -= 
                  CheckedConditionChanged;

            checkedCondition = value;

            if (checkedCondition != null)
            {
                checkedCondition.ConditionChanged += 
                  CheckedConditionChanged;

                ApplyCheckedToAll(checkedCondition.Value);
            }
        }
    }
    
    public bool CanExecute
    {
        get
        {
            if (enabledCondition != null)
                return enabledCondition.Value;
            else
                return true;
        }
    }

    public event ActionExecutedDelegate ActionExecuted;
}

Przykład

Zobaczmy, w jaki sposób wykorzystać tak przygotowany mechanizm.

Załóżmy, że mamy program z przyciskiem, który powinien być dostępny tylko wówczas, gdy zaznaczony jest jakiś element listy oraz gdy włączony jest znajdujący się na formatce checkbox. Tworzymy aplikację z formatką, na której znajduje się przycisk, listbox i checkbox. Pierwszym krokiem jest przygotowanie naszej architektury stanów, warunków i akcji. Ponieważ jest to funkcjonalność, która powinna działać "za kurtyną", większą jej część umieściłem w pliku Form1.Designer.cs.

Pierwszym krokiem jest przygotowanie odpowiednich pól klasy formatki. W pliku Form1.Designer.cs dopisujemy następujące linijki:

  // Wstawione przez designera
  private System.Windows.Forms.Button button1;
  private System.Windows.Forms.ListBox listBox1;
  private System.Windows.Forms.CheckBox checkBox1;

  // Te dopisujemy
  private State listboxSelectionState;
  private State checkboxCheckedState;

  private Condition buttonActionAvailable;

  private Action buttonAction;
}

Następnie przygotowujemy metodę, która zainicjuje wszystkie obiekty naszego mechanizmu.

private void InitializeActions()
{
  listboxSelectionState = new State(false);

  checkboxCheckedState = new State(false);

  buttonActionAvailable = new Condition();

  // Tutaj definiujemy, które stany będą
  // wpływały na dany warunek.
  buttonActionAvailable.AddState(
    listboxSelectionState);
  buttonActionAvailable.AddState(
    checkboxCheckedState);

  buttonAction = new Action();

  // Przypinamy do akcji kontrolkę

  buttonAction.AddControl(button1);

  // Definiujemy warunek dostępności
  // akcji

  buttonAction.EnabledCondition = buttonActionAvailable;

  // Określamy metodę wywoływaną
  // po wywołaniu akcji.

  buttonAction.ActionExecuted += buttonAction_ActionExecuted;
}

Zgodnie z ostatnią linijką potrzebna nam będzie metoda buttonAction_ActionExecuted. Jej zadaniem będzie wykonanie akcji dostępnej po wciśnięciu przycisku. Jednak tutaj kończy się część działań "za kurtyną", więc ograniczymy się w jej implementacji do wywołania metody, która znajdzie się w pliku Form1.cs.

void buttonAction_ActionExecuted(object Action, System.EventArgs e)
{
  DoExecuteAction();
}

Tyle zmian w pliku Form1.Designer.cs. Przejdźmy teraz do Form1.cs. Na początku zaimplementujmy naszą akcję.

private void DoExecuteAction()
{
  MessageBox.Show("Akcja wykonana!");
}

Teraz wzbogaćmy konstruktor o wywołanie naszej metody inicjującej akcje. Zauważmy, że akcje odwołują się do zdarzeń przypinanych do nich kontrolek, więc – co ważne – kontrolki muszą istnieć, gdy konfigurujemy akcje. Dlatego też metoda inicjująca akcje wywołana zostanie już po wywołaniu metody inicjującej komponenty.

public Form1()
{
  InitializeComponent();
  InitializeActions();
}

Na koniec pozostało nam tylko oprogramowanie aktualizacji stanów, gdy zmienione zostanie zaznaczenie listy plików lub gdy zmieni się stan checkboxa.

private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
{
  listboxSelectionState.Value = listBox1.SelectedIndex >= 0;
}

private void checkBox1_CheckedChanged(object sender, EventArgs e)
{
  checkboxCheckedState.Value = checkBox1.Checked;
}

I to wszystko. Wielką zaletą rozwiązania jest fakt, że definicja zależności (która z reguły raz napisana, nie będzie się już zmieniać, a przynajmniej nieczęsto) siedzi w Form1.Designer.cs, zaś faktyczny kod aplikacji zamyka się w czterech metodach, z których najdłuższa ma dwie linijki. Sprawdźmy, czy wszystko działa.

Form1 - kontrolka wyłączona
Form1 - kontrolka włączona

Chętnie wysłucham pomysłów na udoskonalenie powyższego mini-frameworka. Zachęcam do komentowania.

mar
27

Reusability? Tak, ale z głową

Spook, Marzec 27 2011

Napisz komentarz

Moim pierwszym pecetem był 486 DX/33 wyposażony w dysk twardy o pojemności 300 Mb oraz w 8 Mb RAMu. Choć wydaje się, że jest to bardzo mało, konfiguracja ta była w zupełności wystarczająca, by uruchomić na niej Windows 95. Ponieważ w czasie, który wspominam, funkcjonowała jeszcze zasada Moore’a, kolejny sprzęt, który postawiłem na biurku był już wyposażony w procesor Intel Celeron (800 Mhz) oraz 392 Mb RAMu, co umożliwiło zainstalowanie na nim bodaj najpopularniejszego do tej pory systemu, Windowsa XP.

Współczesne komponenty komputerów stacjonarnych – w porównaniu z czasami, o których wcześniej wspomniałem – są wyjątkowo tanie. Szukałem ostatnio cen RAMu; 4 Gb (czyli maksymalna ilość, którą można zaoferować 32-bitowemu systemowi) całkiem przyzwoitej firmy kosztuje w tej chwili około 160 PLN. Gwałtownego spadku cen doświadczyłem też kupując dyski twarde. Obecnie mam zainstalowane trzy: 80, 160 i 250 Gb. Wszystkie są tej samej firmy, wszystkie kupiłem praktycznie w takiej samej cenie (błąd około 10 PLN), tyle że pomiędzy poszczególnymi zakupami mijał mniej więcej rok czasu. W tej chwili za tą samą cenę można kupić dysk o pojemności 1 Tb.

Kiedyś jednak cena jednego bajtu – czy to zaszytego w kości pamięci, czy też na dysku twardym – była znacznie wyższa. Komputery PC były bardzo popularne, ale programista pisząc programy musiał brać pod uwagę, iż będą uruchamiane w środowiskach ubogich w zasoby; na porządku dziennym była walka o każdy, pojedynczy bajt używanej pamięci operacyjnej.

Wówczas właśnie zrodziła się idea reusability. Polegała ona z grubsza na osiąganiu oszczędności podczas korzystania z zasobów poprzez wielokrotne ich używanie. Sprowadzało się to na przykład do wielokrotnego używania jednej zmiennej do wielu różnych celów. Tym sposobem zadeklarowany na początku bloku int i najpierw służył jako iterator w pętli, następnie stawał się pośrednikiem dla danych wprowadzanych do programu przez użytkownika, by zakończyć życie jako zmienna przechowująca sumę elementów potrzebnych do obliczenia średniej jakiegoś zestawu danych. Uderzało to znacząco w czytelność kodu źródłowego, ale było też czasami jedyną metodą na zrealizowanie zamierzonego celu. Powiem więcej – metoda ta funkcjonuje z powodzeniem do dnia dzisiejszego w sytuacjach, gdy zasoby dostępne dla programisty są mocno ograniczone – na przykład podczas oprogramowywania różnego rodzaju prostych procesorów.

Współczesne trendy programowania bardzo mocno odbiegają od niegdysiejszych, jeśli weźmiemy pod uwagę ilość zużywanych zasobów. Dosyć powiedzieć, że programy na popularne w tym momencie systemy operacyjne dla urządzeń mobilnych (Android, Windows Phone 7) pisze się teraz w językach przeznaczonych dla maszyn wirtualnych, nie zaś w języku kompilowanym do rozkazów procesora. Kiedyś dla wszystkich systemów opartych na Windows CE można było pisać w C++, teraz programy dla Windows Phone 7 uruchamiane są na wirtualnej maszynie .NET postawionej na urządzeniu. Programy dla Androida, z kolei, pisze się w Javie. Żadną tajemnicą jest fakt, iż programy takie pracują nieco mniej wydajnie, niż gdyby ich rozkazy wykonywał sam procesor. Ale jakie ma to znaczenie w erze, w której na rynek niebawem ma zostać wprowadzony telefon z dwurdzeniowym procesorem i spadek wydajności programu jest prawie nieodczuwalny?

Ponieważ dawna reusability (w kontekście implementacji kodu źródłowego) straciła sens, współcześnie pojęcie reusability zostało uogólnione i jest interpretowane w zupełnie inny sposób. Programowanie stało się bardzo istotną gałęzią przemysłu, więc jak grzyby po deszczu zaczęły powstawać różne techniki pozwalające na zwiększenie wydajności pracy programistów i ograniczenie kosztów produkcji oprogramowania. Jedna z nich polega na skupieniu się na modularnej konstrukcji architektury programu. Każdy z modułów musi w jak najmniejszym stopniu zależeć od innych, jednocześnie realizując pewien zamknięty zestaw zlecanych mu zadań. Tym sposobem istnieje możliwość wyekstrahowania go z jednego projektu i włączenia w drugi, w którym istnieje potrzeba zrealizowania podobnej funkcjonalności. Ponowne użycie tego modułu (reuse) pozwala na znaczne ograniczenie czasu potrzebnego na realizację kolejnego projektu, a to właśnie czas jest teraz najdroższym elementem rozwoju oprogramowania.

Mimo iż pierwotna reusability (w kontekście oprogramowania dla komputerów PC) nie ma zwykle większego sensu i współcześnie jest uznawana za technikę, która poprzez obniżenie czytelności kodu źródłowego przyczynia się do zwiększenia zasobów potrzebnych do realizacji projektu, spotkałem się ostatnio z kilkoma przypadkami jej zaistnienia – których skutki były opłakane.

Mówię teraz o lokalizacji programów, czyli o dostosowaniu ich do różnych języków i kultur, a ściślej – o procesie tłumaczenia interface’u użytkownika. Proces ten odbywa się poprzez ekstrakcję wszystkich ciągów znaków, które pojawiają się w programie, a następnie przetłumaczeniu ich na inny język i włączeniu do zasobów programu. Tym sposobem raz napisany i skompilowany program może być uruchamiany w wielu różnych wersjach językowych – włącznie z przełączaniem ich w trakcie jego pracy. Przykładem takiego programu jest ProCalc 2 dostępny w dziale download: w zależności od wersji językowej systemu operacyjnego uruchomi się on z napisami po polsku, po włosku lub – w każdym innym wypadku – po angielsku.

Tłumaczenie interface’u niesie ze sobą również zagrożenia związane z tym, że gramatyki różnych języków są zbudowane w odmienny sposób. Polacy znają tylko trzy czasy, tymczasem ktoś naliczył się u Anglików i Amerykanów aż czterdziestu dziewięciu różnych wariacji na temat czasu przeszłego, teraźniejszego i przyszłego (a czasem nawet ich kombinacji). Z drugiej strony Polak jest w stanie odmienić rzeczownik przez siedem przypadków, podczas gdy Amerykanin korzysta tylko z dwóch – mianownika i – rzadziej – dopełniacza (poprzez dodanie ‚s). Innymi słowy, wyrażenie zapisane tak samo w jednym języku, a mające kilka znaczeń, w innym może wyglądać w każdym przypadku inaczej. Na przykład „Elements saved” oznacza: „Elementy zostały zapisane”. Ale jeśli wyrażenie to wystąpi po liczbie, na przykład: „10 elements saved”, należy je przetłumaczyć nieco inaczej: „10 elementów zostało zapisanych”.

Okazuje się, iż wielu projektantów oprogramowania nie bierze powyższego faktu pod uwagę i podczas ekstrakcji ciągów znaków stosują starą zasadę reusability. Jeśli więc w kilku miejscach w programie (ba, czasem nawet w kilku programach) występuje fraza „Elements saved”, do biblioteki tłumaczeń włączają oni to wyrażenie tylko raz i stosują je wielokrotnie. Tym sposobem tłumacz postawiony jest przed zadaniem niemożliwym do zrealizowania, bo – jak pokazałem wcześniej – nierealne jest przygotowanie tylko jednej wersji tłumaczenia, która będzie pasowała wszędzie.

Wydawałoby się, że opisany przeze mnie problem jest oczywisty i żaden rozsądnie myślący programista nie dopuści do jego powstania. Niestety, najwyraźniej tak nie jest. Otóż bowiem tak doświadczony w projektowaniu wielojęzycznych interface’ów użytkownika developer, jakim jest Microsoft popełnił w swoim programie takie oto tłumaczenie (mowa o wersji release oprogramowania):

Zachęcam do zgadnięcia, o jakim programie mowa, jakie tłumaczenie powinno wystąpić w tym miejscu i z czego wynika zabawna pomyłka tłumacza.

Nieprawidłowe tłumaczenie jest tylko jednym przypadkiem nieprawidłowego stosowania opisanej przeze mnie techniki. W pracy analizowałem kiedyś kod, który pisał dla nas zewnętrzny programista. Stosował on tam archaiczne reusability bez skrępowania i argumentował to zwiększeniem wydajności programu i zmniejszeniem zajmowanych przez niego zasobów. Przesłał nam też tytuł książki, na której bazował wszystkie „optymalizacje” wprowadzone do swojego kodu. Okazało się, iż książka ta traktuje o optymalizowanie kodu dla procesorów Pentium i 486 i została wydana w 1997 roku. Sporo mieliśmy pracy z poprawieniem jego algorytmów tak, by dało się z nimi później pracować.

Nie należy lekceważyć technik związanych z reusability. Pozwalają one na realne ograniczenie czasu pracy programistów. Nawet pierwotną reusability można stosować z powodzeniem w niektórych przypadkach. Jednak – jak to w życiu bywa – technika niewłaściwie zastosowana bardzo szybko obraca się przeciw jej użytkownikowi, utrudniając tym samym pracę nad projektem.

Reusability? Zdecydowanie tak. Ale z głową.

sie
23

.NET Compact Framework w Visual Studio Express

Spook, Sierpień 23 2010

Skomentowany 5 razy

Ostatnio bardzo często spotykam się w Internecie z pytaniem, czy istnieje możliwość pisania w wersjach Express Visual Studio programów na platformę .NET Compact Framework. Oficjalnie jest to niemożliwe: polityka marketingowa Microsoftu zadecydowała o włączeniu do środowiska wsparcia dla .NET CF dopiero od wersji Professional (dla zainteresowanych, około 3.5k PLN za jedną licencję BOX). Okazuje się jednak, że taka możliwość istnieje. Zaznaczam, iż pomysł nie jest mój, ale mimo usilnych poszukiwań, nie udało mi się odnaleźć pierwotnego artykułu, na bazie którego przygotowałem moje środowisko pod kompilację dla urządzeń mobilnych.

Rozwiązanie jest następujące:

Ekran Windows Mobile z .NET CF Assemblies

  1. Instalujemy na urządzeniu mobilnym .NET Compact Framework 3.5; jeśli takowego nie posiadamy, albo z innego powodu nie chcemy instalować na nim środowiska uruchomieniowego, możemy posłużyć się darmowym emulatorem urządzenia Windows Mobile. Nie zapomnijmy tylko o doinstalowaniu obrazów systemu Windows Mobile. Dodam tylko, że emulator jest uruchamiany z linii poleceń.
  2. Odszukujemy w urządzeniu, w katalogu \Windows serię plików GAC_*.dll (zobacz screen). Każdy z nich stanowi pojedynczą assembly dla .NET CF. Kopiujemy wszystkie (uważamy tylko na wersję – jest w nazwie pliku) na komputer stacjonarny i umieszczamy w wygodnym dla nas katalogu (w moim przypadku jest to D:\Dokumenty\C\C#\CF_Assemblies\3.5).
  3. Zmieniamy nazwy plików według klucza:GAC_mscorlib_v3_5_0_0_cneutral_1.dllnamscorlib.dll
  4. Upewniamy się, że na komputerze jest zainstalowane .NET Framework 3.5 (będziemy korzystali z kompilatora csc.exe, dołączanego do pakietu)
  5. W katalogu projektu C#, który będziemy chcieli kompilować dla .NET CF tworzymy plik wsadowy o następującej treści:
    set compilerdir="C:\windows\Microsoft.NET\Framework\v3.5"
    set asmdir="D:\Dokumenty\C\C#\CF_Assemblies\3.5"
    
    set projectdir="D:\Dokumenty\C\C#\CF\Hello World\"
    %compilerdir%\csc.exe /noconfig /nostdlib /debug+ /optimize-
      /define:DEBUG;MOBILE /out:"%projectdir%HelloWorld.exe"
      /r:%asmdir%\mscorlib.dll /r:%asmdir%\System.dll
      /r:%asmdir%\System.Data.dll /r:%asmdir%\System.Drawing.dll
      /r:%asmdir%\System.Windows.Forms.dll "%projectdir%Form1.cs"
      "%projectdir%Form1.Designer.cs" "%projectdir%Program.cs"
    
    pause
  6. Dla wygody możemy skonfigurować Visual Studio w taki sposób, by po wywołaniu polecenia z menu External Tools był wywoływany plik compile.bat z katalogu projektu. Zadanie to pozostawimy jednak jako ćwiczenie dla studenta :)

I to by było na tyle. Efekt działania przykładowej aplikacji można podziwiać poniżej.

Przykładowy program .NET CF

Na koniec kilka notatek. Po pierwsze, musimy cały czas mieć na uwadze, że o ile VS C# Express nie będzie nam przeszkadzało w projektowaniu aplikacji dla .NET Compact Frameworka, to z drugiej strony nie będzie nam też pomagało. Mam tu na myśli na przykład fakt, iż designer formatki będzie ją projektował pod .NET dla win32, więc – na przykład – po narysowaniu przycisku na formatce i próbie kompilacji dla .NET CF kompilator przerwie kompilację z następującym komunikatem:

Kompilator Microsoft (R) Visual C# 2008 w wersji 3.5.30729.4926
dla programu Microsoft (R) .NET Framework w wersji 3.5
Copyright (C) Microsoft Corporation. Wszelkie prawa zastrzeżone.

d:\Dokumenty\C\C#\CF\Hello World CF\Form1.Designer.cs(51,26): error CS1061:
        Element "System.Windows.Forms.Button" nie zawiera definicji
        "UseVisualStyleBackColor", a nie odnaleziono metody rozszerzającej
        "UseVisualStyleBackColor", która przyjmuje pierwszy argument typu
        "System.Windows.Forms.Button" (czy nie brakuje dyrektywy using lub
        odwołania do zestawu?).

W takiej sytuacji wystarczy zajrzeć do Form1.Designer.cs i usunąć linijkę, w której następuje przypisanie do nieistniejącej w CF własności UseVisualStyleBackColor.

Sprytny programista wpadnie pewnie na jeszcze jeden ciekawy pomysł – otóż assemblies .NET CF można dołączać jako referencje (References) do projektu. Korzyści z takiego rozwiązania są ogromne – natychmiast po dodaniu zacznie działać Code Insight, podpowiadając nam namespace’y, klasy i ich metody oraz własności, co jest bezcenne w przypadku assemblies nie występujących w przypadku win32 – przykładowo Microsoft.WindowsMobile.PocketOutlook.dll

Zachęcam do dzielenia się opiniami na temat programowania dla .NET CF w Visual Studio Express. Może wymyślicie jeszcze ciekawsze usprawnienia?