Gadu gadu Skype

Archiwum dla tagu: ‘.NET’

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.

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?