Gadu gadu Skype

Archiwum dla kategorii ‘Programowanie’

mar
10

Git z P4Merge

Spook, Marzec 10 2015

Napisz komentarz

Gdyby ktoś miał ochotę używać P4Merge z Gitem, to jest chyba najszybszy sposób. Otwieramy globalny plik konfiguracyjny Gita (C:\Users\<nazwa>\.gitconfig) i dopisujemy (lub modyfikujemy) następujące sekcje:

[merge]
tool = p4mergetool
[mergetool "p4mergetool"]
cmd = \"C:\\Program Files\\Perforce\\p4merge.exe\" \"$BASE\" \"$LOCAL\" 
   \"$REMOTE\" \"$MERGED\"
[diff]
tool = p4mergetool
[difftool "p4mergetool"]
cmd = \"C:\\Program Files\\Perforce\\p4merge.exe\" \"$LOCAL\" \"$REMOTE\"
lut
12

Zabezpieczanie oprogramowania

Spook, Luty 12 2012

Skomentowany 1 raz

Właśnie wypuściłem wersję 1.0.0 ProCalca. Program ten funkcjonuje w dwóch trybach: darmowym (prostym) i rozszerzonym; ten ostatni dostępny jest po zarejestrowaniu programu poprzez instalację pliku licencyjnego. Z uwagi na fakt, iż część programu jest zablokowana przed dostępem dla każdego użytkownika, zacząłem myśleć o tym, w jaki sposób go zabezpieczyć.

Zacząłem więc szukać w informacji w Internecie – od rozwiązań prostych, darmowych do bardziej skomplikowanych, płatnych. Myślałem o tym, jakiego rodzaju zabezpieczenia mogę wprowadzić, i w jaki sposób będzie można byłoby je złamać. Przeczytałem sporo artykułów w poszukiwaniu skutecznej metody zabezpieczenia programu. I wiecie, do jakiego doszedłem wniosku?

Nie da się tego zrobić.

Sprawa jest bardzo prosta. W momencie publikacji programu wszystkie jego binaria stają się dostępne użytkownikowi końcowemu, który z natury rzeczy może z nimi zrobić cokolwiek zapragnie. W praktyce więc każde zabezpieczenie jest on w stanie złamać – jedyną kwestią jest czas, w jakim się to stanie. Jeżeli ktokolwiek powie Ci coś innego – kłamie.

Zaakceptowanie powyższego stwierdzenia przyszło mi ze sporym trudem. Piszę programy hobbystycznie w wolnym czasie, wkładam w nie całe serce i gdy ktoś łamie moje oprogramowanie, czuję się, jakby wyjmował mi pieniądze z kieszeni. Nic dziwnego więc, że do kwestii zabezpieczeń podszedłem z dosyć osobistym nastawieniem. Ale fakty są faktami: moje oprogramowanie zostanie złamane. Kropka.

Nie wygląda to zbyt różowo, prawda? Ale poczytałem trochę więcej o strategiach zabezpieczania programu i okazuje się, że rozwiązania problemu nie należy szukać w paranoicznych zabezpieczeniach, tylko we właściwej strategii jego dystrybucji.

Po pierwsze, zdefiniujmy jasno nasz target. Są ludzie, którzy naszego programu nie kupią. Jeśli ktoś przygotuje cracka, to będą używali pirata, jeśli nie, to go nie będą używać w ogóle. Na tych osobach po prostu nie zarobimy. Z drugiej jednak strony mamy firmy, które nie mogą pozwolić sobie na pracę na pirackim oprogramowaniu oraz ludzi, który nie chcą tego ryzykować, uważają takie zachowanie za niewłaściwe albo choćby nie mają dostatecznego doświadczenia, aby znaleźć crack do programu. I to są właśnie nasi docelowi klienci: nawet jeśli zabezpieczenia programu zostaną złamane, oni go kupią.

Druga sprawa: trzeba mieć cały czas na uwadze, że istotą crackowania programu jest zdejmowanie zabezpieczeń. Czyli osoba, która używa pirata żadnego z tych zabezpieczeń nie zobaczy na oczy – w przeciwieństwie jednak do uczciwego klienta, który nasz program kupił. W efekcie każdy pojedynczy DRM, który zostanie dodany do naszego programu utrudnia życie osobom, które kupiły nasze oprogramowanie.

Kiedyś kupiłem sobie grę – był to bodaj Tomb Raider Anniversary. Zainstalowałem, uruchamiam (oczywiście klucz podany, płyta w napędzie i tak dalej), a tu gra mi mówi, że się nie uruchomi, bo na komputerze znajduje się ProcMon (to takie narzędzie diagnostyczne, bardzo pomocne w pracy programisty, ale można go też używać do łamania zabezpieczeń). I koniec, nie byłem w stanie uruchomić gry. Skończyło się na tym, że przeszukałem Internet i znalazłem cracka. O ironio! Musiałem łamać zabezpieczenia gry, za którą uczciwie zapłaciłem. Jest to chyba najlepszy dowód na bezsens windowania mechanizmów DRM w aplikacji.

Przeciwko pisaniu skomplikowanych zabezpieczeń przemawia jeszcze jeden argument: czas spożytkowany na ich pisanie to czas, który spędziliśmy na walce z wiatrakami zamiast na pisaniu naszego oprogramowania. Użytkownik nie kupi programu, który jest wprawdzie zabezpieczony przed niepowołanym dostępem lepiej niż sam Pentagon, ale liczba zabezpieczeń przewyższa liczbę jego praktycznych funkcji.

Jaką więc strategię przyjąć podczas walki z łamaniem zabezpieczeń naszego oprogramowania?

Pierwszym krokiem jest całkowite wyeliminowanie jednego z naszych przeciwników; mowa tu o tzw. keygenach, czyli programach, które są w stanie wygenerować prawidłowe klucze licencyjne do naszego programu. Keygen to bardzo duży problem; cracker łamie program raz, a potem napisany przez niego generator działa dla każdej wersji naszego programu. Gdy w takiej sytuacji zdesperowani zmienimy algorytmy odpowiedzialne za weryfikację kluczy, nagle okaże się, że musimy wysłać wszystkim dotychczasowym klientom nowe klucze, co po pierwsze jest kłopotliwe z punktu widzenia logistyki, a po drugie – stanowi niedogodność dla tych ostatnich, którzy nagle są zmuszeni do ponownej rejestracji programu.

Dobra wiadomość jest taka, że zabezpieczenie się przed keygenami jest stosunkowo łatwe – wystarczy zastosować zwykłe szyfrowanie asymetryczne: szyfrujemy kluczem prywatnym licencję (lub jej skrót), a w programie zaszywamy klucz publiczny. Program jest wówczas w stanie rozszyfrować i zweryfikować licencję, ale sam klucz publiczny (który wprawdzie można łatwo wyciągnąć z zasobów programu) nie wystarcza, żeby wygenerować nową licencję. Pirat ma dwa wyjścia: spróbować złamać szyfr albo ukraść nam klucz prywatny. Jeżeli tylko zabezpieczymy odpowiednio ten drugi, to drugą połowę roboty odwali za nas matematyka algorytmiki szyfrowania asymetrycznego. Dosyć powiedzieć, że złamanie algorytmu pokroju RSA w rozsądnym czasie jest praktycznie niemożliwe.

Postawiony w takiej sytuacji cracker zostanie zmuszony do obejścia zabezpieczenia zamiast jego złamania, co prawdopodobnie w końcu mu się uda. Efektem jego pracy będzie crack, czyli program, który modyfikuje pliki naszego programu tak, by zabezpieczenia przestały działać, bądź wręcz same zmodyfikowane pliki.

Wiemy już, że nie uda nam się rozwiązać tego problemu bezpośrednio. Ale możemy to zrobić w inny sposób: często wypuszczając nowe wersje programu (nawet z niewielkimi zmianami). Jeżeli program zawiera inteligentnie napisany mechanizm aktualizacji, to końcowy użytkownik nie odczuje za bardzo częstych release’ów, ale dla crackera będzie to już spory problem, ponieważ będzie zmuszony do łamania każdej wersji z osobna i wypuszczania nowych cracków – szczególnie, gdy w mechanizmach zabezpieczających wprowadzimy jakieś niewielkie zmiany.

Jest jeszcze jeden aspekt, o którym się nie wypowiedziałem: cena. Jeżeli promowanemu programowi ustalimy bardzo wygórowaną cenę, więcej osób zdecyduje się na korzystanie z kopii pirackich zamiast z wersji pełnych. Ewolucję cen we właściwym kierunku da się zauważyć na rynku gier na urządzenia mobilne. Kiedyś gry na Nokię N-Gage kosztowały po 150-200 PLN. Teraz gry, które są o kilka rzędów bardziej rozwinięte technologicznie w odpowiednich sklepach (Android Market, App Store itd.) można kupić po 3 PLN. Jeżeli ktoś może taką grę kupić za trzy złote, to nie opłaca mu się tracić czasu na szukanie wersji pirackiej – i tu programista wygrywa z piratem. Warto popatrzeć na to jeszcze z tej perspektywy: łatwiej znaleźć 1000 osób, które zapłacą złotówkę niż jedną, która zapłaci 1000 PLN.

Fakt istnienia oprogramowania pirackiego wbrew pozorom ma trochę zalet. Programiści i wydawcy są zmuszeni wyjść naprzeciw użytkownikom, a poza tym – bądźmy szczerzy – piractwo stanowi również formę promocji oprogramowania. Mam jednak nadzieję, że niebawem rynek oprogramowania zmieni się w taki sposób, że piractwo przestanie się po prostu opłacać.

Edit: Kilka dyskusji wartych przeczytania.

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.

cze
11

Attach to process w Visual Studio Express?

Spook, Czerwiec 11 2011

Napisz komentarz

Potrzebowałem dziś przedebuggować assembly zawierającą komponent, który nie działał prawidłowo w design-time. W takiej sytuacji trzeba przypiąć się jedną instancją Visual Studio do drugiej, co umożliwi postawienie pułapek w assembly i przygwożdżenie błędnego kodu.

Problem polega jednak na tym, że Visual Studio w wersji Express nie udostępnia możliwości przypięcia się do procesu nie będącego jednym z aktualnie edytowanych projektów. Okazuje się jednak, że można ten brak obejść. W tym celu:

  • Dodajemy do solucji nowy projekt – na przykład aplikację konsolową;
  • Zapisujemy go na dysku i zamykamy Visual Studio
  • Edytujemy plik projektu (.csproj), dodając do niego na początku następujące linijki:
    <PropertyGroup>
    <StartAction>Program</StartAction>
    <StartProgram>C:\Program Files\Microsoft Visual Studio 10.0\
    Common7\IDE\VCSExpress.exe</StartProgram>
    </PropertyGroup>
    
  • Zapisujemy, po czym ponownie uruchamiamy Visual Studio
  • Próba uruchomienia tak spreparowanego projektu spowoduje uruchomienie drugiej instancji środowiska, którą można już debuggować.

Oryginalny pomysł: yuvalw

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?

lis
1

StringMatchesMask

Spook, Listopad 1 2007

Napisz komentarz

Potrzebowałem ostatnio funkcji, która sprawdzi, czy nazwa pliku pasuje do zadanej maski. W Internecie w wielu miejscach można znaleźć funkcję StringMatchesMask – jej pierwotna wersja leży chyba na Torrym. Problem polega jednak na tym, że stosuje strategię zachłanną podczas interpretacji gwiazdki, co powoduje z kolei, że stwierdzi, że nazwa adas.txt nie pasuje do wzorca *as.*.

Pozostało mi zatem napisanie poprawionej, rekurencyjnej wersji odpowiedniej funkcji. Rekordów prędkości pewnie nie pobije, ale za to działa poprawnie. Oto jej kod:

function StringMatchesMask(S, mask: string; 
  case_sensitive: Boolean = false): Boolean;

  function InternalStringMatchesMask(PS, PMask: PChar): boolean;

  begin
    while PMask^ <> #0 do
    begin
      case PMask^ of
        '?': begin
            if PS^ = #0 then
            begin
              result := false;
              exit
            end;
            inc(PMask);
            inc(PS);
          end;
        '*': begin
            inc(PMask);
            if PMask^ = #0 then
            begin
              result := true;
              exit
            end;

            while (PS^ <> #0) do
            begin
              if not (PMask^ in ['?', '*']) and (PMask^ <> PS^) then
              begin
                inc(PS);
                if PS^ = #0 then
                begin
                  result := false;
                  exit
                end;
              end else
              begin
                if InternalStringMatchesMask(PS, PMask) then
                begin
                  result := true;
                  exit
                end else
                begin
                  inc(PS^);
                  if PS^ = #0 then
                  begin
                    result := false;
                    exit
                  end;
                end;
              end;
            end;

            result := false;
            exit
          end;
      else begin
          if PMask^ = PS^ then
          begin
            inc(PMask);
            inc(PS);
          end else
          begin
            result := false;
            exit
          end;
        end;
      end;
    end;
    if PS^ = #0 then
      result := true else
      result := false;
  end;

begin
if case_sensitive then
   result:=InternalStringMatchesMask(PChar(s),
                                     PChar(mask)) else
   result:=InternalStringMatchesMask(PChar(UpperCase(s)),
                                     PChar(UpperCase(mask)));
end;