x:DefaultBindMode v Uno Platform a jeho implementace

Minulý týden jsem publikoval článekx:DefaultBindMode v UWP XAMLu. Uno Platform, multiplatformní UWP/WinUI bridge pro Android, iOS WebAssembly, v tu dobu ještě tuto funkci neimplementoval. Co se však stalo potom bylo doslova fascinující. Za pouhý jeden den byla tato funkce nejen implementována, ale i mergnuta (spolu s několika dalšími vylepšeními jako je BindBack!) a dostupná v novém prerelease balíčku na NuGetu. Wow!

Uno Platform

Ukázka na WASM

Použijeme jednu z ukázek z mého minulého článku:

<StackPanel x:DefaultBindMode="OneWay">
<TextBox Text="{x:Bind ViewModel.Name, Mode=TwoWay}" />
<TextBlock Text="{x:Bind ViewModel.Name}" />
</StackPanel>

view raw
OverrideDefault.xaml
hosted with ❤ by GitHub

A po spuštění na WebAssembly s Uno Platform to jednoduše funguje – TextBlock používá OneWay binding, zatímco TextBox jej explicitně přepisuje s TwoWay. To samé na všech ostatních Uno Platform targetech.

x:DefaultBindMode in action on WebAssembly!
x:DefaultBindMode v akci na WebAssembly!

Jak to dokázali?

Pro zajímavost se podívejme, jak tým Uno Platform dokázal za tak krátkou dobu x:DefaultBindMode implementovat.

Vše, co tato funkčnost vyžaduje, je zahrnuto v třídě XamlFileGenerator, která zajišťuje generování C# kódu z XAMLu.

x:DefaultBindMode funguje v jako “scope”. To znamená, že deklarovaný mód na elementu platí pro něj a všechny jeho potomky, pokud není explicitně přepsán někde níže. Protože XAML soubor je parsován postupem “do hloubky”, nejvhodnější datovou strukturou pro reprezentaci scope je zásobník. A to také Uno Platform používá pro udržování stavu zde:

/// <summary>
/// The current DefaultBindMode for x:Bind bindings, as set by app code for the current Xaml subtree.
/// </summary>
private readonly Stack<string> _currentDefaultBindMode = new Stack<string>(new[] { "OneTime" });

view raw
BindModeStack.cs
hosted with ❤ by GitHub

Všimněme si, že je zásobník inicializován s hodnotou OneTime – to odpovídá výchozímu módu x:Bind v UWP. Takže pokud nikde nepoužijeme x:DefaultBindMode, OneTime je výchozí “fallback”.

Jak vložíme nový “výchozí mód” na zásobník? Zde přichází v činnost metoda TrySetDefaultBindMode:

private IDisposable TrySetDefaultBindMode(XamlObjectDefinition xamlObjectDefinition)
{
var definedMode = xamlObjectDefinition.Members.FirstOrDefault(m => m.Member.Name == "DefaultBindMode")?.Value?.ToString();
if (definedMode == null)
{
return null;
}
else if (!IsValid(definedMode))
{
throw new InvalidOperationException(
"Invalid value specified for attribute 'DefaultBindMode'. Accepted values are 'OneWay', 'OneTime', or 'TwoWay'.");
}
else
{
_currentDefaultBindMode.Push(definedMode);
return new DisposableAction(() => _currentDefaultBindMode.Pop());
}
bool IsValid(string mode)
{
switch (mode)
{
case "OneWay":
case "OneTime":
case "TwoWay":
return true;
default:
return false;
}
}
}

view raw
TrySetDefault.cs
hosted with ❤ by GitHub

Nejprve metoda přečte hodnotu atributu DefaultBindMode z definice elementu. Pokud atribut na elementu není deklarován, metoda skončí a vrací null. Pokud deklarování je, proběhne jeho validace pomocí metody IsValid.

Pokud je hodnota DefaultBindMode validní, vložíme ji na vrchol zásobníku (vstupujeme do nového scope). Metoda pak vrací instanci DisposableAction, která implementuje rozhraní IDisposable a při jejím Dispose() vykoná předanou lambda funkci. V tomto případě se odebere vrchní hodnota ze zásobníku.

Tento způsob implementace skvěle napomáhá čitelnosti kódu, protože to umožňuje jednoznačně ukázat, že metoda vytváři nový “scope”, když ji voláme uvnitř using bloku:

using (TrySetDefaultBindMode(xamlObjectDefinition))
{
}

view raw
UsingBindMode.cs
hosted with ❤ by GitHub

Poznamenám ještě, že v případě že metoda vrátí null (pro případ kdy DefaultBindMode není nastaven), je prostě ignorován a using se nepokouší na konci bloku volat Dispose().

Získat aktuálního mód je možné přes jednoduchou metodu která volá pouze Peek aby vrátila vrchol zásobníku:

private string GetDefaultBindMode() => _currentDefaultBindMode.Peek();

view raw
GetDefault.cs
hosted with ❤ by GitHub

Metoda GetDefaultBindMode() je pak volána při stavbě vyhodnocovací funkce x:Bind:

var modeMember = bindNode.Members
.FirstOrDefault(m => m.Member.Name == "Mode")?.Value?.ToString() ?? GetDefaultBindMode();

view raw
Mode.cs
hosted with ❤ by GitHub

Jednoduše řečeno, pokud výraz x:Bind explicitně určí Mode, má přednost. V opačném případě použije aktuálně platný DefaultBindMode.

A to je vše, přátelé! Jak jsme mohli vidět, celá funkce byla implementována velmi elegantně a díky Uno Platform máme opravdu #WinUIEverywhere!

Buy me a coffeeBuy me a coffee

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.