Gadu gadu Skype
lut
27

From past to present

Spook, Luty 27 2012

Napisz komentarz

Dla miłośników Skyrima – efekt robienia wszystkiego poza tym, co się powinno…

Podgląd w MuseScore

From past to present

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.

sty
30

Piotr Hosowicz nie żyje

Spook, Styczeń 30 2012

Napisz komentarz

Dowiedziałem się chwilę temu, że po długiej walce z chorobą zmarł Piotr „Lodzisław” Hosowicz. Nie jest to osoba znana i pojawiająca się w światłach reflektorów, więc pozwolę sobie nakreślić krótko jego postać – jakim widziałem go sam.

Uczestniczył w życiu grup dyskusyjnych od bardzo długiego czasu – pamiętam, że był jednym z grupowiczów, którzy odpowiedzieli na mój pierwszy post na pl.comp.lang.delphi. Zawsze pomagał, zawsze służył dobrą radą, pomagał cierpliwie wszystkim początkującym – nie wahał się podrzucić kilku dobrych pomysłów nawet wówczas, gdy był w środku swojej pracy.

Był też jednym z organizatorów wieloletnich Zlotów Programistów Delphi, z których zawsze miałem wielką frajdę i z których – w dużej mierze za jego przyczyną – wyjeżdżałem zawsze pełen entuzjazmu do dalszego zgłębiania tajników programowania.

Piotr "Lodzisław" Hosowicz

Odnalazłem jego zdjęcie ze Zlotu 2008 z Krakowa i prawdę mówiąc takim go najlepiej pamiętam. Zawsze w akcji, dopinający wszystko na ostatni guzik, żeby wszystko się udało i żebyśmy wszyscy mogli wynieść z całej akcji jak najwięcej dobrego.

Żal, gdy tak wartościowi ludzie odchodzą. Do zobaczenia po drugiej stronie, Lodzisławie!

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ą.

mar
12

Nokia i Microsoft – co z tego wyniknie?

Spook, Marzec 12 2011

Napisz komentarz

Ostatnimi czasy interesuję się wszelkimi wydarzeniami mającymi miejsce w świecie urządzeń, technologii i oprogramowania mobilnego. Nie mógł więc ujść mojej uwagi bardzo kontrowersyjny alians Nokii z Microsoftem.

Jeśli przyjrzymy się historii Nokii z przeciągu ostatnich pięciu lat, alians ten okaże się znacznie mniejszym zaskoczeniem niż mogłoby się to na początku wydawać. Cofnijmy się na chwilę do roku 2007. Nokia jest u szczytu swoich możliwości, kończy rok ze sprzedanymi przeszło 437,1 milionami telefonów (drugi jest Samsung utrzymujący się na poziomie 161,2 milionów, źródło). Pod koniec października 2007 roku kurs akcji Nokii osiąga wartość nie notowaną od pięciu lat – blisko 40 USD. I aż trudno uwierzyć, że właśnie w takich okolicznościach zaczyna się wielki upadek tej popularnej firmy.

Jest 9 stycznia 2007 roku. Apple, po długim okresie domysłów i plotek ogłasza, iż pracuje nad projektem nowego, rewolucyjnego telefonu – iPhone’a. Musi minąć jeszcze przeszło pół roku, żeby telefon ten trafił na rynek, jednak marketing Apple’a robi swoje: ludzie ustawiają się w kolejkach przed sklepami nawet na sto godzin przed premierą, stoisk z nowymi telefonami spod znaku jabłuszka w nocy poprzedzającej premierę strzeże policja i ochrona. iPhone – mimo dosyć wysokiej ceny – sprzedaje się w zastrzaszającym tempie: 74 dnia po premierze został sprzedany milionowy egzemplarz.

Na czym polegał sukces Apple’a? Oczywiście marketing odegrał tu swoją rolę. Jednak Apple uderzył w rynek innowacją: postawił na ekran dotykowy (wprawdzie takie telefony były już wówczas na rynku, ale niezbyt popularne), skonstruował atrakcyjny, efektowny i użyteczny interface użytkownika przystosowany do obsługi palcami oraz wyeksponował multimedialne cechy urządzenia: bardzo duży ekran (jeśli dobrze pamiętam, największy w urządzeniu tego typu w czasie premiery) oraz dużo pamięci przeznaczonej do przechowywania multimediów. Urządzenie nie było bez wad – do głównych zaliczany był brak schowka, niemożność wysyłania MMSów, a także zamknięcie możliwości instalowania oprogramowania firm trzecich, które nie przeszło procesu cyfrowej certyfikacji, nie przeszkodziły one jednak w wielkim sukcesie Apple’a.

Czy iPhone był naprawdę tak dobry, żeby zacząć wypierać z rynku telefony Nokii? Myślę, że raczej nie. Nokia była w stanie zaprojektować i wypuścić na rynek telefon o podobnych (albo nawet i lepszych) parametrach i pozbawiony wad iPhone’a. Jednak w kierownictwie Nokii ktoś popełnił poważny błąd, ponieważ z wielkiego sukcesu iPhone’a nie wyciągnięto wniosków i Nokia nie zmodyfikowała swojej rynkowej strategii, aby sprostać nadchodzącym wyzwaniom.

Co dzieje się dalej? Nokia wydaje na świat model 5800, który stał się pierwszym telefonem z ekranem dotykowym, który został przyjęty pozytywnie przez rynek (Nokia 7700 nigdy nie weszła do sprzedaży, zaś 7710 z Symbianem S90 nie odniósł większego sukcesu). Telefon nie miał jednak szans konkurować z iPhone – był raczej ewolucją niż rewolucją w stosunku do poprzednich modeli, ponadto do użytkowania potrzebował czasem rysika, podczas gdy iPhone został zaprojektowany pod obsługę przy pomocy palców. Ponadto sukcesowi marketingowemu – jak na złość – przeszkodziła wadliwa konstrukcja słuchawki, która przestawała działać po bardzo krótkim czasie. Nokia oczywiście zadziałała szybko i umożliwiła darmową wymianę wadliwego komponentu, jednak wydarzenie to z pewnością wpłynęło na statystyki sprzedaży.

Po umiarkowanym sukcesie modelu 5800 Nokia postawiła na telefony z serii N (od eNtertainment) – lata 2008 i 2009 przyniosły modele N78, N79, N85, N96, a także N97. Jednak wszystkie stanowiły tylko wariacje na temat: zmieniała się wersja systemu, parametry sprzętowe i rozdzielczość wbudowanego aparatu fotograficznego, ale nadal brakowało czegoś nowego, rewolucyjnego.

Tymczasem konkurencja Nokii nie spała, a wręcz przeciwnie – różne firmy prześcigały się w pomysłach na „iPhone killera”.

W trzecim kwartale 2009 roku pojawia się popularny Samsung Omnia. Czwarty kwartał to premiery telefonów z Androidem, między innymi Motorola Droid oraz HTC Hero. Początek roku 2010 zaznaczył się wejściem do sprzedaży telefonu Google Nexus One, a niedługo potem Sony Ericsson zaatakowało rynek XPerią X10. I dopiero po tych wszystkich premierach, pod koniec 2010 roku Nokia zaczyna wprowadzać telefony nowej generacji, z systemem Symbian^3: Nokię N8, a niedługo potem C7 i E7 (która, moim zdaniem niesłusznie, odziedziczyła po świetnej serii handheldów miano Communicator).

Rynek był jednak bezlitosny wobec Nokii i długi okres stagnacji, gdy fińska firma wypuszczała jeden po drugim telefony różniące się głównie wyglądem, mocno odznaczył się w historii finansowej firmy. Wystarczy tylko popatrzeć na kurs akcji Nokii w okresie od 2007 roku do dziś.

Źródło: www.dailyfinance.com

Jest początek roku 2011. Nokia systematycznie traci zainteresowanie rynku, konkurencja co chwilę wprowadza telefony, które budzą wielkie zainteresowanie, zaś premiery fińskiego producenta przechodzą bez większego echa. Nokii potrzeba rewolucji – czegoś, co wstrząsnie rynkiem. I w takich właśnie okolicznościach decyduje się ona na sojusz z Microsoftem.

Nie zdziwiłem się specjalnie, że Nokia wykonała tak gwałtowny ruch, ponieważ było to dla niej mniej więcej jak być lub nie być, natomiast zachodziłem w głowę, dlaczego fiński producent nie zdecydował się na bardzo popularnego Androida, tylko na odsuniętego w tło Windows Phone 7. Wszystko stało się jasne, gdy przeczytałem, kim był wcześniej obecny CEO Nokii, pan Stephen Elop.

Proponuję zobaczyć, jak zareagował rynek na opublikowaną 11 lutego decyzję o aliansie Nokii i Microsoftu.

Moim zdaniem decyzja Nokii to zdecydowany strzał w kolano. Po pierwsze, system Microsoftu nie zyskał zbyt dużej popularności; najlepszym epitetem, którego można użyć do określenia interface’u użytkownika WP7 jest „ascetyczny”. Wystarczy zobaczyć, jak wyglądają pewne wspólne elementy każdego systemu mobilnego w Symbianie, Androidzie i Windows Phone 7:

Ekran główny
Menu
Kontakty
Ustawienia

Screenshoty z Nokia RDA, Android Emulator oraz dzięki uprzejmości MicrosoftFeed.com

Windows Phone nie przegrywa jednak z innymi systemami tylko z powodu swojego interface’u. Większym problemem jest fakt, iż wszystkie technologie Microsoftu są zamknięte, tymczasem zarówno Symbian jak i Android są systemami otwartymi, w każdej chwili można z Internetu ściągnąć kompletne kody źródłowe każdego z nich.

Jest jeszcze jeden problem. Miarą współczesnego systemu operacyjnego dla urządzeń mobilnych jest liczba developerów, którzy piszą dla niego oprogramowanie. Nokia udostępnia SDK Symbiana darmowo na swoich stronach, a poza tym na Symbiana można pisać w QT. Poza tym programy napisane dla Symbiana można bez problemu publikować i każdy może instalować je bez większych ograniczeń (w przypadku programów korzystających np. z połączeń telefonicznych lub wysyłających SMSy wymagana jest certyfikacja). Google również udostępnia kompletny SDK dla Androida; ba, istnieje nawet odpowiedni plugin do Eclipse’a pozwalający na łatwe projektowanie i programowanie aplikacji. Oczywiście programy również można bez ograniczeń publikować w postaci pakietów apk.

Microsoft jednak nie przepuścił okazji do zysku. Wprawdzie, jeśli chodzi o środowisko programistyczne, pobił na głowę zarówno Symbiana, jak i Androida, ponieważ udostępnił darmowo specjalną wersję Microsoft Visual Studio przeznaczoną do pisania programów dla Windows Phone 7. Na tym jednak kończy się otwartość tej firmy: żeby zainstalować napisany przez siebie program na fizycznym urządzeniu, trzeba je najpierw odblokować. Aby jednak mieć taką możliwość, trzeba być zarejestrowanym developerem Windows Phone 7, co wiąże się z rocznymi opłatami (obecnie $99). Ale i na tym nie koniec; odblokować można tylko kilka urządzeń do celów deweloperskich, natomiast publikowanie programów może odbywać się tylko przez Microsoft Marketplace. Warto wspomnieć o jeszcze jednym ograniczeniu: zarejestrowany deweloper może opublikować tylko 100 darmowych programów, za publikację każdego kolejnego musi zapłacić $19.99 (ograniczenie to nie dotyczy programów płatnych).

Jestem fanem Nokii od mojego pierwszego telefonu. Przez moje ręce przeszły modele 3210, 3510i, N-Gage, 6230i, E66 i C7. Obawiam się jednak, że moja przygoda z tą marką ma się już ku końcowi. Ponieważ zbliża się termin przedłużenia umowy abonamentowej, mam możliwość wybrania nowego telefonu i będzie nim najprawdopodobniej HTC Desire Z lub HTC Desire HD z Androidem na pokładzie – zainstalowałem już Eclipse i Netbeans, aby nauczyć się pisać aplikacje na tą platformę.

Strategia Nokii jest bardzo ryzykowna. Będziemy musieli jednak poczekać jeszcze trochę, by dowiedzieć się, czy firma ta zaskoczy nas niedługo jakimś nowym, rewelacyjnym telefonem, który pozwoli jej wrócić na falę, czy jednak Windows Phone 7 stanie się ostatnim gwoździem do trumny tego bardzo popularnego niegdyś producenta komórek.

paź
10

Resetowanie ustawień ActiveSync

Spook, Październik 10 2010

Napisz komentarz

Czasami zdarza się potrzeba zrobienia twardego restartu palmtopa i zreinstalowania wszystkich aplikacji. Niestety, Microsoft ActiveSync nie jest dostatecznie sprytny i wykryje palmtopa jako nowe urządzenie – dodając tym samym kolejny partnership. Jest to dosyć irytujące tym bardziej, że nie zostanie mu przypisana nazwa naszego urządzenia (bo takowy już istnieje). Wystarczy zastosować jednak prosty trick, by usunąć poprzednie partnerstwo i utworzyć nowe przy kolejnej synchronizacji. W tym celu należy:

  • Przechodzimy do katalogu C:\Documents and Settings\\Dane Aplikacji\Microsoft\ActiveSync\Profiles i usuwamy podkatalog odpowiadający poprzednio utworzonemu partnerstwu;
  • Uruchamiamy edytor rejestru, przechodzimy do gałęzi Mój komputer\HKEY_CURRENT_USER\Software\Microsoft\Windows CE Services\Partners i usuwamy podgałąź odpowiadającą poprzedniemu partnerstwu – można to sprawdzić, odczytując klucz DisplayName (partnerstwa są zapisane w postaci liczby heksadecymalnej, na przykład, 63ef340a)

Podczas następnej synchronizacji zostanie utworzone nowe partnerstwo o takiej samej nazwie, jak nazwa urządzenia.

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?

paź
11

Sudety

Spook, Październik 11 2008

Napisz komentarz

Stoisz na rozstaju dróg. Jest słoneczny, letni poranek, rosa skrzy się na trawie; na nitkach pajęczyny wygląda jak przezroczyste perły. Kropelki wody porozwieszane pomiędzy gałęziami sprawiają wrażenie, jak gdyby ktoś przyozdobił drzewa diamentami. Rozglądasz się; widzisz rozlane, niewielkie leśne jeziorko. Jego powierzchnia jest gładka, marszczona gdzieniegdzie przez poranną bryzę, która niesie Ci zapach lasu. Na brzegu – postrzępiony kapelusz wędkarza, który skoro rano wybrał się na wczesny połów. Obracasz się powoli – słońce przebłyskujące pomiędzy gałęziami drzew wydaje się świecić na zielono. Otacza Cię soczysta, głęboka, żywa zieleń przyprószona tu i ówdzie błękitnymi plamkami niezapominajek albo czerwonym kapeluszem muchomora. Robisz kilka kroków: żwir chrzęści pod Twoimi stopami, a wygrzewające się na poboczu ropuchy niechętnie ustępują Ci drogi. Trącasz jakąś trawę z której zrywa się latająca paleta kolorów – motyl. Robi przed Tobą kilka wdzięcznych ósemek, po czym odlatuje w zacienione, jeszcze, gąszcze lasu.

Stoisz na skale wysuniętej poza zbocza wzgórza na które przed chwilą mozolnie się piąłeś. Rozglądasz się – widzisz głęboki, gęsto zadrzewiony wąwóz, na dnie którego płynie cicho pomarańczowy – od wypłukiwanych tu i ówdzie złóż żelaza – strumyk. Czujesz podmuch wiatru, ale nie jest to już spokojna bryza, ale żywy wiatr tych wzgórz. Zamykasz oczy – i nie słyszysz już szumu drzew, ale czujesz go podróżując wraz z wiatrem, który wciągnął Cię w szaloną podróż. Przelatujesz wąwozem, pokonujesz przełęcz i mkniesz ponad zielonymi płaskowyżami ku dalekim wzniesieniom. A może zawracasz – by przez zielone wrota wylać się na szerokie równiny pokryte szachownicami zasianych pól, czerwienią dachówek pokrywających domy okolicznych wiosek, i błękitem niewielkich jeziorek i strumyków?

Skręcasz z drogi, wijącej się wśród wąwozów i przełęczy by nagle, ku swojemu zdziwieniu, pomiędzy szczytami pobliskich wzniesień dostrzec szeroko rozlane jezioro. Zamyka je tama – jak gdyby zawieszona na sąsiednich szczytach. Na jednym z brzegów pastelowymi kolorami mienią się niewielkie domki, spokojne fale syzyfową pracą próbują podmyć piaszczyste wybrzeże. Wchodzisz na rozchybotany, wiszący mostek. Przed Tobą, w oddali bieli się żagiel, pod Tobą przepływa mała łódka; pozostawia za sobą kilwater, który znika powoli, rozmywając się coraz bardziej na lustrzanej płaszczyźnie. Wchodzisz na tamę; z jednej strony widzisz jezioro, druga znów kusi Cię, byś wzleciał i jak myszołów pomknął nad głęboko werżniętą w góry dolinką, widząc od czasu do czasu stadka krów i owiec albo konopiastogrzywą klacz pasącą się na łące.

***

A Teraz siedzisz w pociągu i z rozrywającym serce żalem widzisz Twoje wzgórza oddalające się od Ciebie w rytm stukotu kół na szynach. Zaciśnięta kurczowo na poręczy ręka drży, bo chciałbyś wyrwać się, uciec, wrócić – choć wiesz, że nie możesz. Jedziesz odwiedzić cywilizację: rozkopane ulice, brudne dzielnice z dziećmi bawiącymi się w błocie podwórek; długie oczekiwania na zatłoczony autobus; ciągnące sie w nieskończoność korki – zrezygnowane i zmęczone twarze ludzi; betonowo-stalowe zbrojenia, które teraz muszą zastąpić Ci korzenie drzew; falujące, wzburzone morze nieznajomych, często wrogich Ci twarzy.

A Twoje wzgórza będą czekać… najpierw postarzeje się wiatr – i zamiast hasać radośnie pośród drzew zacznie zrywać ich liście, które pokryją kolorowym dywanem leśną ściółkę. Potem uśnie, otoczony płynącą bielą, która zagłuszy leśne bicie serca. Drewniane szkielety będą skrzypieć, gnąc się pod naporem śniegu, skarżąc się – świadome swej bezsilności – na samotniczy los.

A później zbudzi się życie – zielone pędy będą walczyć na zacienionej przez starszyznę ziemi o swój promyk słońca, drzewa wypełnią się zielenią, a wiatr znów będzie zapraszał do podróży.

A Ty? Przystaniesz czasami zdumiony, bo w głębi miasta poczujesz nagle zapach zieleni, zapach wolności, która wciąż ciągnie Cię w Twoje strony. A to tylko niegościnny, miejski wiatr zagarnął w Twoje nozdrza złudny zapach z pobliskiego parku…