Víceméně pořád pracuji na nějakém projektu, který je určen pro .NET Framework 4.0 a současně na projektu, který může používat vlastnosti z nejnovější verze. Také mám nasbíráno spoustu užitečných nástrojů a metod a všechno to mám schované v knihovně, kterou ke každému svému projektu připojím. Idea je, že projekty na novější verzi používají kód napsaný přímo v té verzi a projekty na starších verzích používají stejný nebo podobný kód používající vlastnosti ze starší verze. Nějakou dobu jsem neohrabaně vždycky kus kódu zakomentoval, zkompiloval pro jednu verzi a pak jsem zase zakomentoval něco jiného a zkompiloval pro druhou verzi. To je ale velmi pomalé a otravné řešení a navíc pro tento problém existuje v .NET Frameworku a Visual Studiu řešení.
CallerMemberName atribut
Jako příklad uvedu třídu ViewModelBase, kterou používám ve WPF projektech jako base třídu pro všechny moje view modely (zatím jsem si vystačil bez MVVM frameworku). Přestože tématem článku není WPF, popíši následující ukázku podrobněji, než by bylo nutné, aby ji pochopili všichni.
.NET 4.0
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
Metoda SetField<T>(T, T, string) je pomocná metoda, která nejdříve ověří, jestli se hodnota opravdu změnila, abych nevolal OnPropertyChanged(string) zbytečně. Tuto metodu pak volám přímo v setteru property mého view modelu.
private string title;
public string Title
{
get { return title; }
set { SetField(ref title, value, "Title"); }
}
.NET 4.5
Od .NET 4.5 je k dispozici nový atribut
CallerMemberName, který automaticky dodá jméno volající property nebo metody jako argument.
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
// tady se nic nemění
}
Potom není nutné specifikovat toto jméno manuálně a riskovat tak překlep.
set { SetField(ref title, value); }
Toto samozřejmě chci mít ve své knihovně a chci mít možnost nespecifikovat jméno property, pokud dělám v projektu cíleném na verzi 4.5.1.
Build configuration a preprocessor directives
Projekt, ve kterém se kód nachází, nechám nastavený normálně na starší verzi frameworku. Ve Visual Studiu si nejdříve vytvořím novou build konfiguraci v Configuration Manageru, který najdete buď v menu Build a nebo na panelu nástrojů jak je ukázáno na obrázku.
Kromě odkazu na něj jsou tam právě všechny build konfigurace spojené s projektem. Debug a Release jsou tam vždy, NET45 je můj, který teď vytvoříme. V Configuration Manageru rozbalte první combobox a vyberte volbu <New…> . Do řádku zajdete jméno, doporučuje se bez mezer, ideálně kombinace malých písmenek, velkých písmenek a číslic. Dále pak v comboboxu vyberte konfiguraci, která bude základem pro vytvoření této. Já jsem použil Release, protože neobsahuje některé prvky určené pro debugging. Potvrďte a nastavte Vašemu projektu tuto konfiguraci. Protože jsem nenašel, kde se dá pro build konfiguraci nastavit cílená verze .NET Frameworku, tak jsem projekt zavřel a editoval jeho .csproj soubor, kde jsou tyto informace uloženy. Ten si otevřete klidně v notepadu a vyhledejte následující kousek konfigurace.
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'NET45|AnyCPU'">
<OutputPath>bin\NET45\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<Optimize>true</Optimize>
<DebugType>pdbonly</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<ErrorReport>prompt</ErrorReport>
<CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
Tady je potřeba udělat dvě věci. Nejdříve definovat konstantu, kterou později budeme používat, v elementu DefineConstants. Pokud jste profil vytvořili na základě Release konfigurace, pak je tam stejně jako u mě TRACE. Tato konstanta povoluje používání metod ve třídě System.Diagnostics.Trace, které lze používat například pro logování. V zdrojovém kódu této třídy můžete u některých metod vidět aplikovaný následující atribut
[System.Diagnostics.Conditional("TRACE")]
který definuje, že daná metoda půjde zavolat, jen pokud je nastavená konstanta TRACE. Pokud metody této třídy nepoužíváte, lze ji smazat. V opačném případě ji oddělíme středníkem a přidáme vlastní konstantu, jejíž jméno by se mělo držet konvence – použití velkých písmen, číslic a jako oddělovač podtržítko. U mě je to RUNNING_NET_4_5. Dále je potřeba přidat element pro cílenou verzi frameworku.
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
Při používání profile verzí (např. 4.0 Client Profile) je potřeba uvést i následující element.
<TargetFrameworkProfile>Client</TargetFrameworkProfile>
Soubor uložíme a otevřeme projekt ve VS. Nyní aplikujeme námi definovanou konstantu v kódu. Protože potřebuji změnit pouze signaturu metody a to jen poslední parametr, udělám následující
protected bool SetField<T>(ref T field, T value,
#if RUNNING_NET_4_5
[System.Runtime.CompilerServices.CallerMemberName] string propertyName = null)
#else
string propertyName)
#endif
{ ... }
Používám zde preprocessor direktivu #if, která funguje na stejném principu jako if, který používáte v kódu. Je důležité, aby na řádku s direktivou nebylo nic víc kromě direktivy a případně konstanty. Nejdříve tedy specifikuji, že pokud je nastavená moje konstanta, pak poslední parametr bude dekorován atributem CallerMemberName a v opačném případě tam nebude. Specifikuji celý název typu (tj. včetně namespace), abych tento namespace nemusel v souboru uvádět mezi ostatními usingy.
To je vše. Teď už stačí jen projekt buildnout a ve složce bin se objeví nová složky pro danou konfiguraci (toto lze také nastavit) a tam assembly buildnutá pro verzi 4.5.1 s metodou zvýhodněnou vyšší verzí frameworku. Pokud ve VS konfiguraci změním zpět na Release a buildnu projekt, pak se mi ve složce bin/Release objeví assembly pro verzi 4.0 s metodou, která používá dostupnou funkcionalitu.
Build konfigurace i samotné preprocessor direktivy jsou oboje mocné nástroje, které lze používat nezávisle na sobě. Direktivami lze například nastavovat hodnoty kontakt v kódu a měnit tak cesty k souborům podle konfigurace (např. TEST, CUSTOMER, ale klidně i Debug a Release) a pomocí build konfigurace mohu nastavit mnohem více věcí, z nichž některé jsou dostupné ve vlastnostech projektu, na záložce Build a jiné můžete dodefinovat ručně. Lze tak třeba měnit reference na projekty nebo odebrat celý soubor z projektu. Možností je spousta.