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?

EntityFramework i wartość domyślna wyliczana na podstawie funkcji

Domyślne wartości bardzo łatwo ustawić za pomocą wizualnego edytora EntityFramework. Wystarczy ustawić właściwość Default w oknie properties:

Clipboard01

Co jednak gdy chcemy ustawić wartość wyliczoną na podstawie jakieś funkcji? Dla przykładu może być to DateTime.Now bądź też Guid.NewGuid()?Wpisując w te same okienko dostaniemy błąd podczas kompilacji:

Error    1    Error 54: Default value (System.Guid.NewGuid()) is not valid for GUID. The value must be enclosed in single quotes in the form 'dddddddd-dddd-dddd-dddd-dddddddddddd'.    Persons.edmx    446    11    Crm

Na szczęście rozwiązanie jest bardzo proste – wystarczy stworzyć klasę Partial i w konstruktorze ustawiać wartość domyślną:

public partial class Person
{
    public Person()
    {
        this.ID_PERSON = Guid.NewGuid();
    }
}

Takim sposobem możemy manipulować danymi w dowolny sposób. Wszelkie konwersje danych nie są teraz problemem.

ComboBox w DataGrid oraz brak wsparcia SelectedValue w Silverlight

Siliverlight niestety nie jeszcze tak potężną biblioteką jak WPF. Czasami proste rzeczy wymagają większego nakładu pracy niż w przypadku tworzenia aplikacji w WPF.

Rozważmy sytuację, w której mamy listę telefonów do danej osoby.Telefon definiujemy poprzez jego numer oraz przydzieloną kategorię(dom, praca itp). W DataGrid będą więc 2 kolumny: jedna typu TextBox dla numeru oraz druga typu ComboBox dla kategorii. DataGrid będzie przedstawiał listę telefonów opisanych klasą Phone:

class Phone
{
    public string Value{get;set;}
    public PhoneType PhoneType{get;set;}
}

gdzie PhoneType to:

class PhoneType
{
    public string Value{get;set;}// nazwa kategorii
    public Guid ID_PHONE_TYPE{get;set;}
}

Zrzut z gotowej kontrolki DataGrid:

Clipboard01

W wpf kod byłby bardzo prosty a mianowicie:

<data:DataGrid   Name="m_Phones" ItemsSource="{Binding Path=Contact.Phones}" AutoGenerateColumns="False">
    <data:DataGrid.Columns>
                           
        <data:DataGridTextColumn Header="Phone" Binding="{Binding Path=Value}"/>
                            
            <data:DataGridTemplateColumn Header="PhoneGroup">
                <data:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <ComboBox SelectedValuePath="ID_PHONE_TYPE" SelectedValue="{Binding Path=ID_PHONE_TYPE"  DisplayMemberPath="Value" ItemsSource="{StaticResource AllPhoneTypes}">
                           </ComboBox>
                    </DataTemplate>
                </data:DataGridTemplateColumn.CellTemplate>

        </data:DataGridTemplateColumn>

    </data:DataGrid.Columns>
</data:DataGrid>

SelectedValuePath wskazuje na identyfikator item’a w ComboBox. SelectedValue służy do zaznaczania aktualnej kategorii.Wszystko w wpf ładnie będzie nam się odświeżać. Załadowane telefony automatycznie będą zaznaczać kategorię w ComboBox.

Niestety w Silverlight brakuje właściwości SelectedValue oraz SelectedValuePath.Wszystko co możemy zrobić za pomocą xaml to:

<data:DataGrid   Name="m_Phones" ItemsSource="{Binding Path=Contact.Phones}" AutoGenerateColumns="False">
    <data:DataGrid.Columns>
                           
        <data:DataGridTextColumn Header="Phone" Binding="{Binding Path=Value}"/>
                            
            <data:DataGridTemplateColumn Header="PhoneGroup">
                <data:DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <ComboBox DisplayMemberPath="Value"    ItemsSource="{StaticResource AllPhoneTypes}">
                           </ComboBox>
                    </DataTemplate>
                </data:DataGridTemplateColumn.CellTemplate>

        </data:DataGridTemplateColumn>

    </data:DataGrid.Columns>
</data:DataGrid>

W SL do dyspozycji mamy wyłącznie właściwość SelectedItem jeśli chodzi o zaznaczanie pozycji w ComboBox. Niestety ItemsSource dla ComboBox pochodzi z innego źródła niż aktualnie wybrana kategoria. Nie możemy zatem użyć poniższego kodu:

<ComboBox SelectedItem="{Binding Path=PhoneType}" DisplayMemberPath="Value"  ItemsSource="{StaticResource AllPhoneTypes}"/>

Musimy powiązać SelectedItem za pomocą ID_PHONE_TYPE a nie instancji klasy PhoneType.Problem polega na tym, że SelectedItem spodziewa się typu PhoneType a nie GUID. Co więc możemy zrobić?

Na szczęście  SilverLight wspiera konwertery IValueConverter. Umożliwiają one podczas bindingu zamianę jednej wartości na drugą. W naszym przypadku zamienimy GUID na klasę PhoneType. Zatem do dzieła, zacznijmy od implementacji tego konwertera:

public class PhoneTypeConverter : IValueConverter
{
    public ObservableCollection<PhoneType> ItemsSource { get; set; }
    
    public object Convert(object value,
    System.Type targetType,
    object parameter,
    System.Globalization.CultureInfo culture)
    {
        Guid phoneTypeId = (Guid)value;
        PhoneType phoneType = null;
        
        foreach (PhoneType item in ItemsSource)
        {
            if (item.ID_PHONE_TYPE == phoneTypeId)
            {
                phoneType = item;
            }
        }
        return phoneType;
    }
        
    public object ConvertBack(object value,
    System.Type targetType, object parameter,
    System.Globalization.CultureInfo culture)
    {
        PhoneType phoneType = value as PhoneType;
        Guid phoneTypeId = Guid.Empty;
        phoneTypeId = phoneType.ID_PHONE_TYPE;
        return phoneTypeId;
    }
}

Konwerter zawiera dwie metody Convert(Guid –> PhoneType) oraz ConvertBack(PhoneType –> Guid).Teraz możemy już użyć SelectedItem:

<ComboBox SelectedItem="{Binding Path=ID_PHONE_TYPE,Converter={StaticResource PhoneTypeConverter}}" DisplayMemberPath="Value"  ItemsSource="{StaticResource AllPhoneTypes}"/>

Konwerter zamieni Guid(ID_PHONE_TYPE) na odpowiednią  instancję PhoneType, która jest już podłączona do ItemsSource. Ostatnią sprawą jest zdefiniowanie zasobu PhoneTypeConverter:

<UserControl.Resources>
   <local:PhoneTypeConverter x:Key="PhoneTypeConverter" ItemsSource={StaticResource AllPhoneTypes} />        
</UserControl.Resources>

Takim to sposobem poradziliśmy sobie z brakiem wsparcia dla SelectedValue w Silverlight. Mam nadzieję, że oszczędziłem komuś męczarni z tym – samemu sporo sie nakombinowałem zanim udało mi się osiągnąć zamierzony  efekt.Dodam, że SilverLight 4 Beta posiada już właściwość SelectedValue więc takie scenariusze będą łatwiejsze w implementacji.

Prawidłowe definiowanie wyjątków w c#

Programując własne biblioteki, często potrzebujemy zdefiniować własny typ wyjątku. Przeglądając różnego rodzaju kody źródłowe nierzadko spotykam błędną deklarację własnych wyjątków:

 public class MyException : Exception
 {
    // specyfikacja
 }

Co prawa kompilator nie zgłosi błędu ale już np. CodeAnalysis zwróci nam uwagę o błędnej deklaracji. Najprościej korzystać w Visual Studio z tzw. snippet’ów czyli gotowych fragmentów kodu. Naciskając klawisze ctrl+space pojawi nam się lista dostępnych snippetów w VS:

snippetsWybieramy oczywiście Exception a automatycznie zostanie wstawiony poniższy fragment kodu:

    [Serializable]
    public class MyException : Exception
    {
        public MyException() { }
        public MyException(string message) : base(message) { }
        public MyException(string message, Exception inner) : base(message, inner) { }
        protected MyException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }
    }

Jak widać deklaracja jest bardziej złożona ponieważ prawidłowa implementacja wyjątku wymaga uwzględnienia mechanizmu serializacji czyli zapisu klasy np. do pliku XML. Potrzebne jest to do wysyłania klasy zdalnie przez sieć.Wyobraźmy sobie sytuacje w której nasza biblioteka zostaje wyeksponowana za pomocą WCF. W takim przypadku jeśli chcemy zwracać wyjątki do klienta muszą one być zapisane do jakiegoś formatu(np. XML) i zwrócone zdalnie klientowi.

Seria webcastów o Visual Studio 2010

Zapraszam wszystkich na webcasty poświecone VSTS. Rejestracja jest darmowa więc tym bardziej warto skorzystać z okazji. Oto lista najbliższych spotkań:

Styczeń 26, 2010: What’s New in VS2010 (Webcast) 
Luty 23, 2010: VSS to TFS: Strategies for Migration 
Marzec 23, 2010: What’s New in VS2010 
Kwiecień 20, 2010: VS2010 Quality Tools for Developers 
Maj 18, 2010: Lab Manager – The Ultimate "No More No Repro" Tool 
Czerwiec 14, 2010: Full Testing Experience: Professional QA with VS2010

Więcej informacji znajdziecie tutaj. Podziękowania także dla Anorak’a za informacje o tych wydarzeniach.

Silverlight i brak wsparcia dla x:Static

Programiści WPF z pewnością kojarzą makro x:Static służące do bindowania m.in. stałych. Bardzo przydatne jest to w przypadku gdy mamy klasę stałych, którą chcemy przypisać pewnym właściościom kontrolek. W PRISM, wykorzystuje się to do definiowania nazw regionów:

<ItemsControl regions:RegionManager.RegionName="{x:Static slbase:RegionNames.NoteView}">

Dzięki takiemu rozwiązaniu nie musze martwić się o aktualziacje nazwy regionu w pliku xaml.

Ostatnio piszę projekt w Silverlight i spotkało mnie niemiłe zaskoczenie gdy okazało się, że SL nie wspiera takiego makra. W przykładach dołączonych do frameworku PRISM autorzy na sztywno podają nazwy. Znalazłem jednak lepsze moim zdaniem rozwiązanie. Możemy uzyć zasobów a mianowicie:

<controls:TabControl cal:RegionManager.RegionName="{Binding Note, Source={StaticResource Regions}}">

W powyższym kodzie bindujemy właściwość Note do RegionName. Wszystko realizowane jest również w sposób statyczny. Wystarczy, że zdefiniujemy jeszcze w ResourceDictionary następujący znacznik:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:slbase="clr-namespace:SlBase.Mvvm;assembly=SlBase">

    <slbase:Regions x:Key="RegionsBase"/>
</ResourceDictionary>

Nie jest to rozwiązanie idealne ale wystarczające. Zasoby możemy zdefiniować w osobnym pliku i  dołączać je w ramach potrzeb co redukuje ilość kodu.

Startujemy

Witam,

Po długim namyśle postanowiłem zacząć prowadzić blog techniczny. Blog poświecony jest głównie szeroko pojętej technologii .NET. Z pewnością nie zabraknie jednak informacji o wzorcach projektowych oraz o pisaniu porządnego kodu. Będę również starał się prezentować frameworki, które ułatwiają pisanie aplikacji klasy enterprise. Planuję także opisywać techniki optymalizacji kodu c# ponieważ uważam, że informację takie  zbyt często są omijane w publikacjach.

Zachęcam więc do regularnego czytania!