Metody częściowe(partial methods)

Z klasami częściowymi większość programistów c# prawdopodobnie miała już styczność. Klasycznym przykładem jest rozbicie klasy Form na część wygenerowaną przez Visual Studio oraz na część przeznaczoną do modyfikacji przez programistę. Metody częściowe są bardzo podobnym mechanizmem. W skrócie są to metody, które można definiować w dwóch różnych plikach. Zdecydowałem się o nich napisać ponieważ wydaje mi się, że są mniej znane, a czasami mogą okazać się przydatne.

Sama deklaracja jest analogiczna do klas częściowym. W jednym pliku deklarujemy metodę:

// Plik A
partial void Method();

W drugim pliku możemy zdefiniować ciało metody:

// Plik B
partial void Method()
{
    MessageBox.Show("Metoda czesciowa");
}

Mechanizm pozwala nam na definicję metody w innym pliku niż jej deklaracja. Metody częściowe mają jedną wyróżniającą cechę – wszelkie wywołania są usuwane na etapie kompilacji w przypadku gdy programista nie zdefiniował ciała tej metody. Dzięki temu kod jest optymalny ponieważ w sytuacji gdy nie było potrzeby definiowania metody, wszelkie wywołania będą usuwane w czasie kompilacji, co skutkuje, że plik końcowy nie będzie zawierał żadnych śladów tej metody.

Metody częściowe najczęściej wykorzystuje się w generatorach kodu. W takim przypadku nie chcemy aby użytkownik kodu modyfikował kod automatycznie wygenerowany(mogło by to zakłócić poprawne działanie tworzonego narzędzia programistycznego). Z drugiej jednak strony, chcemy aby użytkownik mógł modyfikować w jakimś stopniu działanie klasy. Możemy więc zdefiniować pewne metody częściowe, które będą wywoływane w odpowiednich momentach. Użytkownik klasy chcąc wpłynąć na działanie klasy mógłby definiować ciała metod częściowych. Oczywiście podobny rezultat można osiągnąć poprzez dziedziczenie i metody wirtualne, jednak metody częściowe są znacznie bardziej optymalne i przeważnie łatwiejsze w użyciu. Wszystko zależy jednak od konkretnego przypadku użycia – metody częściowe stanowią dodatek a nie próbę zastąpienia dotychczasowych rozwiązań.

BusyIndicator w Silverlight

W Silverlight toolkit można znaleźć bardzo przydatną kontrolkę – BusyIndicator(poprzednia nazwa Activity). Służy ona do zasygnalizowania użytkownikowi, że jakaś operacja jest wykonywana. Na początek screen z gotowej aplikacji:

image

Co warto podkreślić, podczas stanu aktywnego kontrolki użytkownik nie może modyfikować danych, które przedstawia interfejs. Kontrolka działa w trybie modalnym – wszystko w tle jest zablokowane i automatycznie przyciemnione.

Po zainstalowaniu bibliotek z Silverlight toolkit, najlepiej dodać je do toolbox’a . W tym celu klikamy prawym przyciskiem myszki na oknie toolbox a następnie wybieramy “Choose Items”. W oknie przechodzimy do “Silverlight components” i klikamy w przycisk “Browse”. Przechodzimy do katalogu z zainstalowanymi bibliotekami  a następnie zaznaczamy System.Windows.Controls.Toolkit.dll.

Użycie kontrolki sprowadza się do ustawienia właściwości IsBusy na true w przypadku gdy chcemy zasygnalizować użytkownik wykonywaną operację:

<controlsToolkit:BusyIndicator IsBusy="True">

</controlsToolkit:BusyIndicator>

W praktyce oczywiście będziemy korzystać z wiązania danych i jako wartość IsBusy podamy np. {Binding Path=IsBusy}.

Następną istotną sprawą jest fakt, że w kontrolce BusyIndicator możemy zagnieżdżać inne kontrolki, które korzystają ze źródła danych. Wtedy IsBusy przeważnie jest już ustawiane automatycznie przez zagnieżdżoną kontrolkę.

Wygląd i zachowanie BusyIndictator możemy zmieniać dowolnie – podobnie jak resztę kontrolek WPF.

Ustawienie “Culture” w WPF

WPF mam jedną niedogodność – domyślnie wszelkie ustawienia regionalne interfejsu są ustawione na en-US. Oznacza to, że wszystkie domyślne konwertery również korzystają z ustawień en-US. Chcąc wyświetlić np. datę, prawdopodobnie pożądanym będzie przedstawienie daty polskiemu użytkownikowi w formacie dd-mm-yyyy oraz amerykańskiemu w mm-dd-yyyy. Niestety domyślnie właściwość UIElement::Language jest ustawiona na en-US.  Za pomocą prostego kodu możemy jednak zmienić metadane i ustawić prawidłową “kulturę”:

protected override void OnStartup(StartupEventArgs e)
{
    FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(System.Windows.Markup.XmlLanguage.GetLanguage(System.Threading.Thread.CurrentThread.CurrentCulture.IetfLanguageTag)));
    
    base.OnStartup(e);
}

Ustawienie prawidłowych ustawień regionalnych najlepiej umieścić w OnStartup ponieważ wystarczy to wykonać tylko raz – przy starcie aplikacji.

IntTextBox oraz RealTextBox w WPF

Poprawnie zaprojektowana aplikacja powinna weryfikować dane w każdej warstwie systemu. Oczywiście najważniejszym miejscem jest warstwa biznesowa ale dobrym zwyczajem jest walidacja również w warstwie prezentacji. W idealnym interfejsie użytkownik nie jest w stanie wprowadzić błędnych danych. Podstawowym przykładem są pola edycyjne w których powinno się wpisać np. ilość sprzedanego produktu. Użytkownik nie powinien mieć możliwości wprowadzenia tekstu w takie pole.

Zacznijmy od prostej sprawy – akceptacja tylko cyfr.

public class IntNumberTextBox:System.Windows.Controls.TextBox
{
    protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
    {
        e.Handled = !ValidateText(e.Text);
        base.OnPreviewTextInput(e);
    } 
    private bool ValidateText(string text)
    {
        foreach (Char character in text)
        {
            if (System.Char.IsDigit(character) == false)
                return false;
        }
        return true;
    }
}

Jak widać wystarczy tylko przeładować metodę OnPreviewTextInput i ustawić flagę Handled na true w przypadku gdy wprowadzony znak jest niedozwolony.

W przypadku liczb rzeczywistych musimy umożliwić dodatkowo wprowadzanie przecinków lub kropek. Znak oddzielający część dziesiętną od całkowitej zależy od ustawień regionalnych w systemie operacyjnym. W C# możemy sprawdzić jaki to jest znak za pomocą właściwości System.Globalization.NumberFormatInfo.CurrentInfo.NumberDecimalSeparator.

public class RealNumberTextBox:System.Windows.Controls.TextBox
{
   protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
   {
       e.Handled = !ValidateText(e.Text);
       base.OnPreviewTextInput(e);
   }
   private bool ValidateText(string text)
   {
       if ( text == System.Globalization.NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)
                  return true;
       foreach (Char character in text)
       {
           if (System.Char.IsDigit(character) == false)
               return false;
       }
       return true;
   }
}

Inne ciekawe wartości, zależne od ustawień regionalnych, które są przydatne w walidacji to m.in CurrencyDecimalSeparator, CurrencyGroupSeparator, CurrencySymbol, NegativeSign, NegativeInfinitySymbol.

Teraz pozostało nam tylko wykorzystać nowo utworzoną kontrolkę w pliku XAML. Najpierw należy zadeklarować przestrzeń nazw:

<Window x:Class="Sample.Views.Product.Product"
        xmlns:controls="clr-namespace:Sampple.Controls"
        inne deklaracje
>

Następnie korzystamy z kontrolki w sposób analogiczny do standardowych kontrolek:

<controls:IntNumberTextBox HorizontalAlignment="Left" Width="200" Text="tekst" />

Pamiętajmy, że walidacja w dolnych warstwach jest zwykle wolniejsza ponieważ należy połączyć się najpierw np. z bazą danych lub z usługą sieciową. Ponadto korzystanie z aplikacji z walidacją w warstwie prezentacji jest po prostu łatwiejsze dla użytkownika.

Filtrowanie QueryString za pomocą maksymalnej długości

IIS 7.0 ze względu bezpieczeństwa ogranicza długość tzw. QueryString do 2048 bajtów. Czasami okazuje się, że długość jest zbyt ograniczająca i nie da się wykonać pewnych operacji takich jak np. przekazanie tokena, który zwykle zajmuje więcej niż naniesione ograniczenie. Na szczęście w pliku konfiguracyjnym web.config można łatwo ustawić dowolną maksymalną długość QueryString:

<system.webServer>
    <security>
        <requestFiltering>        
            <requestLimits maxQueryString="5000" />        
        </requestFiltering>    
    </security>
</system.webServer>

Dobrą zasadą jest również ustawianie długości jak najmniejszej, tak aby wystarczała tylko na przewidziane przypadki użycia.

Prawidłowe zwalnianie zasobów

W języku c# obiekt, którego zasobami chcemy sami zarządzać, powinien implementować interfejs IDisposable. Sporo osób aby zwolnić zasoby pisze następujący kod:

public class MyClass : IDisposable
{
   #region IDisposable Members

   public void Dispose()
   {
       // zwalnianie zasobow
   }

   #endregion
}

Interfejs niestety wymusza nam tylko implementację metody Dispose. Powyższe rozwiązanie jest zdecydowanie nieprawidłowe. Zanim  jednak przejdę do omawia co w kodzie jest niepoprawnego, podam prawidłową implementację:

public class MyClass:IDisposable
{
    private bool IsDisposed=false;
    public void Dispose()
    {
        Dispose(true);
        GC.SupressFinalize(this);
    }
    protected void Dispose(bool Diposing)
    {
        if(!IsDisposed)
        {
            if(Disposing)
            {
                // zwalniaj zasoby zarządzalne
            }
            // zwalniaj zasoby niezarządzalne
        }
        IsDisposed=true;
    }
    ~MyClass()
    {
        Dispose(false);
    }
}

Powyższa implementacja gwarantuje nam, że gdy obiekt będzie automatycznie czyszczony przez GarbageCollector to zasoby niezarządzane i tak zostaną wyczyszczone. Uzyskujemy to dzięki implementacji destruktora, który wywołuje się zawsze gdy GC zwalnia obiekt.  Właśnie w przypadku gdy to GC zwalnia obiekt a nie użytkownik ręcznie, metoda Dispose nie jest wywoływana więc jedynym miejscem na zrobienie tego jest destruktor.

Następna kwestia to przypadek gdy to sam użytkownik chce zwolnić obiekt. W tym przypadku musimy poinformować GC, że bierzemy na siebie odpowiedzialność za obiekt(metoda SupressFinalize) i tym samym destruktor nie zostanie wywołany(co uchroni nas przed zapętleniem).

Ponadto dodałem flagę IsDisposed, która umożliwia nam np. wyrzucenie wyjątku w przypadku gdy użytkownik drugi raz chce zwolnić obiekt.

Wymuszenie walidacji kontrolek w WPF

Windows Presentation Foundation posiada nowy model walidacji kontrolek. Każdej kontrolce podlegającej walidacji przypisujemy zestaw reguł walidacyjnych, np:

<TextBox  Height="23" Margin="120,6,177,0"  VerticalAlignment="Top" >
    <Binding Path="FirstName" Mode="TwoWay">
        <Binding.ValidationRules>
            <DataErrorValidationRule></DataErrorValidationRule>
        </Binding.ValidationRules>
    </Binding>
</TextBox>

Powyższa reguła mówi, że wykorzystywany obiekt biznesowy wspiera interfejs IDataErrorInfo:

public partial class Person : IDataErrorInfo
{
   private Dictionary<string, string> m_ValidationErrors = new Dictionary<string, string>();
   public Invoice()
   {       
   }
   private void AddError(string columnName, string msg)
   {
       if (!m_ValidationErrors.ContainsKey(columnName))
       {
           m_ValidationErrors.Add(columnName, msg);
       }
   }
   private void RemoveError(string columnName)
   {
       if (m_ValidationErrors.ContainsKey(columnName))
       {
           m_ValidationErrors.Remove(columnName);
       }
   }
   partial void OnFirstNameChanged()
   {
       if (string.IsNullOrEmpty(ReceiverName))
       {
           AddError("FirstName", "Musisz podac imie");
       }
       else
           RemoveError("FirstName");
   }
   
   #region IDataErrorInfo Members

   public string Error
   {
       get
       {
           if (m_ValidationErrors.Count > 0)
           {
               return m_ValidationErrors.ElementAt(0).Value;
           }
           else return null;
       }
   }
   public string this[string columnName]
   {
       get
       {
           if (m_ValidationErrors.ContainsKey(columnName))
           {
               return m_ValidationErrors[columnName];
           }
           else return null;
       }
   }
   #endregion
}

W przykładzie wykorzystałem obiekt Entity Framework. Implementacje wymusza nam interfejs więc myślę, że kod nie wymaga dodatkowego komentarza(jeśli macie jakieś pytania to proszę zadawać je np. w komentarzach).

Inna regułą, która spełnia się gdy zostanie wyjątek wyrzucony podczas próby zmiany właściwości jest ExceptionValidationRule. Ponadto, istnieje możliwość definiowania własnych reguł. Gdy któraś kontrolka nie przejdzie walidacji zostaje domyślnie podświetlona na czerwono:

image

Walidacja zostanie jednak wywoływana pierwszy raz w momencie gdy kontrolka straci focus bądź gdy zostanie zmieniona w niej wartość(zależy to od ustawień bindingu). Co jednak w przypadku gdy chcemy aby przy starcie okna błędnie wypełnione pola(w tym przykładzie puste) zostały zaznaczone na czerwono?

Rozwiązanie jest proste: Wystarczy obsłużyć zdarzenie Windows Loaded oraz wykonać poniższy kod:

m_FirstName.GetBindingExpression(TextBox.TextProperty).UpdateSource();

Po wykonaniu kodu, wszystkie puste pola(czyli te, które nie przeszły walidacji) będą podświetlone i tym samym użytkownik będzie wiedział co należy wypełnić.

Jak dostać się do InnerException w RIA Services

Dziś postanowiłem wyjaśnić problem, który zauważyłem jest często poruszany na forach. Załóżmy, że wywołujemy metodę SubmitChanges aby zatwierdzić wprowadzone modyfikacje. Wykonanie metody jednak nie powodzi się i wyrzucany jest wyjątek. Scenariusz wygląda typowo jednak często otrzymujemy mało mówiący komunikat typu:

Submit operation failed. Exception has been thrown by the target of an invocation.

lub

Submit operation failed. An error occurred while updating the entries. See the InnerException for details.

Co gorsza, jeśli zajrzymy do InnerException zobaczymy wartość NULL. Oczywiście mówimy ciągle o stronie klienta. Pytanie brzmi więc: W jaki sposób możemy podejrzeć co tak naprawdę stało się?

Rozwiązaniem jest przeładowanie metody Submit(DomainService) lub OnError(DomainService) oraz obsłużenie tam ewentualnych wyjątków:

public override bool Submit(ChangeSet changeSet)
{
  try
  {
      return base.Submit(changeSet);
  }
  catch (Exception e)
  {
      throw;
  }
}

Jeśli zajrzymy do InnerException zobaczymy, że wyjątek jest tam zapisany i możemy przeczytać dokładniejszą informację np.

Violation of PRIMARY KEY constraint 'PK_Table.Tests'. Cannot insert duplicate key in object 'TestModule.Tests'.
The statement has been terminated.

Rozwiązanie jest być może niezbyt eleganckie ale działa bez zarzutu.

Aktualizacja interfejsu z drugiego wątku(windows forms i WPF)

Na różnych forach często użytkownicy mają problem z aktualizacją kontrolek z innego wątku. Załóżmy, że odpaliliśmy sobie BackgroundWorker lub po prostu Thread. Wykonujemy jakieś operację, np. łączymy się ze zdalnymi zasobami. Stworzenie osobnego wątku w takim scenariuszu jest bardzo pożądane ponieważ nie blokujemy wtedy aktualizacji interfejsu. W trakcie pobierania informacji z Internetu chcemy aktualizować interfejs aby informować użytkownika o postępach np.

progressBar.Value = progessValue;

Jeśli powyższy kod jest wywołany z obcego wątku użytkownik dostanie następujący komunikat o błędzie:

Cross-thread operation not valid:
Control accessed from a thread other than the thread it was created on.

Wyjątek jest wyrzucany ponieważ zarówno w WindowsForms jak i w WPF nie można aktualizować interfejsu z innego wątku niż z tego w którym została stworzona kontrolka. Musimy więc w jakiś sposób dostać się do wątku macierzystego dla kontrolki i tam wykonać aktualizacje tej kontrolki. Służy do tego metoda Control.Invoke(windows forms) lub ControlDispatcher.Invoke(Wpf). Chcąc więc zaktualizować ProgressBar w WinForms musimy napisać:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
progressBar.Invoke(updateAction,32);

Powyższy kod będzie działał w każdym przypadku ale ma jedną wadę związaną z wydajnością kodu. Co w przypadku gdy mamy osobną klasę do aktualizacji interfejsu i jest ona wywoływana zarówno z wątku kontrolki jak i z obcego wątku? Dla drugiego przypadku(z obcego wątku) kod jest maksymalnie optymalny. Z kolei w sytuacji gdy wywołujemy ją z wątku macierzystego dla kontrolki nie potrzebnie będzie wykonywana masa operacji związanych z wpompowaniem operacji w kolejkę dla wątku interfejsu. Na szczęście istnieje gotowa metoda, która stwierdza czy dla aktualnego przypadku użycia jest wymagane wywołanie Invoke:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
if (progressBar.InvokeRequired)
    progressBar.Invoke(updateAction,5);
else
    updateAction(4);

Posiadamy już optymalną wersje kodu. Jednak pisanie powyższego kodu za każdym razem gdy chcemy zaktualizować interfejs(nigdy nie wiemy czy kod nie będzie wywoływany w przyszłości z innego wątku) jest co najmniej niewygodne. Z ratunkiem przychodzą nam tzw. rozszerzenia(extensions,c# 3.0). Możemy przecież stworzyć własną metodę nazwaną powiedzmy InvokeIfRequired:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (control.InvokeRequired)
           control.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (control.InvokeRequired)
           control.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Za pomocą takiego rozwiązania aktualizacja kontrolki sprowadzi się wyłącznie do poniższego kodu:

this.InvokeIfRequired((value) => progressBar.Value = value, 10);

W przypadku Wpf rozwiązanie jest bardzo podobne:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Z kolei użycie metody jest identyczne jak w przypadku WinForms:

this.InvokeIfRequired((value) => bar.Value = value, 10);

Jak dodać opis wartości ENUM?

Czasami typ ENUM znajduje zastosowanie(czasami ponieważ często ogranicza on modułowość aplikacji). W wielu przypadkach potrzebujemy jednak skojarzyć pewien opis z każdą wartością enum’a. Jako praktyczny scenariusz można wymienić implementację menedżera dźwięków. Dla przykładu w pewnej grze, którą współtworzyłem aby uatrakcyjnić interfejs dla programisty zdefiniowałem sobie typ enumeryczny SOUND_TYPE:

public enum SOUND_TYPE
{
   ROCKET_LAUNCH,
   MACHINE_GUN_LAUNCH
}

Programista zatem chcąc wykorzystać dźwięk przedstawiający strzał rakiety wywołuje następującą metodę:

SoundManager.Instance.PlaySoundEffect(SOUND_TYPE.ROCKET_LAUNCH);

Dla użytkownika kodu wprowadzenie typu ENUM jest bardzo korzystne ponieważ wywołując metodę PlaySoundEffect nie da się wprowadzić błędnego parametru wejściowego. Powyższa definicja ENUM nie definiuje jednak w żaden sposób ścieżek plików dźwiękowych. W tym celu stworzyłem atrybut przypisujący każdemu typowi ENUM ścieżkę prowadzącą do pliku z dźwiękiem:

public enum SOUND_TYPE
{
   [AssetName(AssetName = "rocket_gun_launch")]
   ROCKET_LAUNCH,
   [AssetName(AssetName = "machine_gun_launch")]
   MACHINE_GUN_LAUNCH
}

Deklaracja AssetName to:

public class AssetNameAttribute:Attribute
{
   public string AssetName
   {
       get;
       set;
   }        
}

Mamy więc zdefiniowany typ ENUM oraz przypisaną ścieżkę pliku. Pozostaje nam tylko dostać się do tych danych. W tym celu zdefiniowałem metodę rozszerzającą(extensions, c# 3.0):

public static class EnumEx
{
   static public attr GetAttributeValue<attr>(this Enum primaryEnum) where attr:class
   {
       FieldInfo field = primaryEnum.GetType().GetField(primaryEnum.ToString());
       Attribute[] attrs = field.GetCustomAttributes(typeof(attr), false) as Attribute[];
       if (attrs.Length == 0) return null;
       else
           return attrs[0] as attr;
   }
}

Teraz aby odczytać ścieżkę do pliku dla podanego enum’a wystarczy:

SOUND_TYPE soundType = SOUND_TYPE.ROCKET_LAUNCH;
string filePath = soundType.GetAttributeValue<AssetNameAttribute>().AssetName;

Scenariuszy jest naprawdę wiele na wykorzystanie powyższego rozwiązania. Dla przykładu wyobraźmy sobie, że implementujemy CRM i mamy listę telefonów. W ENUM możemy zdefiniować sobie typy HOME, WORK  itp. Następnie za pomocą własnego atrybutu możemy dodać opis każdej kategorii, który będzie wyświetlany w interfejsie. Za pomocą atrybutów możemy także definiować reguły walidacyjne dla zmiennych(RangeAttribute, RequiredAttribute, DefaultAttribute itp).

Na koniec pozostaje mi wyjaśnienie 2 kwestii.

Po pierwsze zawsze powinniśmy zastanowić się czy aby na pewno warto wprowadzać typ ENUM do budowanego systemu. Wracając do przykładu CRM, co jeśli pewnego dnia okaże się, że wymagana jest trzecia grupa telefonów np. MOBILE? Wprowadzanie jej wymagało by modyfikacji kodu co w aplikacjach klasy enterprise jest niedopuszczalne. Najlepiej podsumowuje ten wniosek, jedno z podstawowych zasad w inżynierii oprogramowania Open\Closed Principle:

A module should be open for extension but closed for modification”

Druga to wytłumaczenie się dlaczego zdecydowałem się użyć SOUND_TYPE w grze skoro tak neguje większość przypadków użycia ENUM. Wynika to po prostu z przyjętych wymagań, technologii oraz czasu na wykonanie gry. Gra została napisana w XNA, która wykorzystuje tzw. Content Pipiline. Nie wchodząc w szczegóły wszelkie zasoby gry(dźwięki, modele, efekty) muszą być przekompilowane i zamienione na specjalny format obsługiwany zarówno na PC jak i XBOX. Skoro zasoby nie mogą być dodawane dynamicznie przez użytkowników(ponieważ muszą być rekompilowane) to po co męczyć się z rozszerzalnym interfejsem, który i tak nigdy nie zostanie wykorzystany?