Označování textu v HTML aneb na počtvrté správně a po svém

Cíle

  1. Označit (resp. zvýraznit) text v prohlížeči (Chrome, Firefox, IE, Edge) a to i v mobilních verzích aplikace (Android, iOS, Windows Phone)
  2. Uložit serializovanou podobu označení do úložiště pro pozdější obnovení označení (například při návratu na stránku)
  3. Při novém načtení stránky (render stránky), označit text

Poprvé

Musím se přiznat, že k prvnímu pokusu o označení jsem pouze přišel s tím, že už to je implementované správně a funguje. Nefungovalo. V daném HTML byli vytvořené jakési „stropy“ (offsety), reprezentované prostým span HTML elementem s číselnou hodnotu v datasetu.

Tento kód zde raději ani nesdílím, neb bych nerad, aby se čtenáři udělalo nevolno.

Nebylo možné označování přes více různých elementů – původně počítalo s označením pouze v odstavcích, ale tabulky, divy nebo obrázky to ignorovalo.

Takhle to nepůjde.

Podruhé

Většinou nejprve hledám po internetu, jestli už někdo neřešil podobný problém. Měl jsem štěstí – řešil. Narazil jsem na knihovnu rangy.js. Má svou wiki, kde je krásně popsáno API, takže jsem se mohl podívat, co tato knihovna umí.

rangy.js má v sobě modul pro zvýraznění (Highlighter module), který má dvě implementace:

  • textContent
    • výhody: jednoduchý, rychlý algoritmus
    • nevýhody: změny DOMu mají za následek, označení něčeho jiného než bylo před změnou DOMu označeno, i jakýkoli bilý znak (white-space) měl za následek mutaci označení
  • TextRange
    • výhody: odolný proti zápisu více bílých znaků (white-space)
    • nevýhody: extrémně pomalý

Implmentace „textContent“ nemohla být použita, protože mobilní aplikace renderovala HTML trochu jinak a to i přesto, že bylo použito Chromium (jádro Chrome tzn. stejný render engine).

Zkusil jsem implementaci „TextRange“. Nepoužitelné. Mobilní aplikaci trvalo až 10s než vyrenderuje označené HTML.

I když bylo označení vždy přesné, tak z výkonových důvodu jsem od něj upustil.

Potřetí

Tento postup, už jsem vymýšlel já. Spoočíval v serializaci Range objektu velmi jednoduchou metodou. Range objekt má v sobě startContainer, startOffset, endContainer a endOffset. Serializace probíhá tak, že se vygeneruje validní selektor elementu startContainer a endContainer relativně ke zvolenému HTML elementu (například k body) a spočítá se startOffset a endOffset v daných start/end kontejnerech.

// Takto vypadá definice selektoru, který lze uložit do databáze jako JSON
interface IHtmlElementSelectorResult {
   selector: string;
   childNodeIndex: number;
   offset: number;
}
// Takto vypadá příklad serializované reprezentace Range objektu
{
   start: {
       selector: "div > div > p:nth-of-type(4)",
       childNodeIndex: 0,
       offset: 10
   },
   end: {
       selector: "div > div > p:nth-of-type(8) > strong",
       childNodeIndex: 0,
       offset: 5
   }
}

Z takto serializované hodnoty, lze velmi snad zrekonstruovat Range objekt a nad ním poté provést označení.

Dokud nezmutuje DOM. Pokud zmutuje, tak není možná rekonstrukce. A právě tím, že chceme označit text, tak DOM mutuje (vkládáme <span> HTML elementy, které zvýrazňují text).

Takže znova a jinak.

Počtvrté

Opět jsem se vyskytl na zelené louce.

S kolegou jsme se nad tímto problémem zamysleli z jiného pohledu. Označení nebude prováděno na základě znalosti (reprezentace) DOMu, ale na základě znalosti počtu výskytu jednotlivých slov, které jsme vybrali, že chceme označit a které se „lámali“ skrze DOM.

Pustil jsem se do implementace. Procházením DOMU a sbíráním počtu výskytu označených slov, jsem došel k cíli. Fungovalo to jak na desktopu, tak i na mobilních aplikacích, je to relativně svižné – PC maximálně malé stovky milisekund, i při označení obrovského kusu textu (DOMu), na mobilních aplikacích podobně.

Využil jsem k procházení DOMu objekt TreeWalker, který jsem si nastavil tak, aby procházel jen ten označený kus textu a spočítal si výskyty slov. Výpočet jsem si uložil do JSON, který ač je závislý na velikosti textu a počtu výskytu slov, tak funguje skvěle.

TL;DR

Pro označení, uložení pozice a opětovné označení textu v HTML (DOM) nejsou vhodné způsoby:

  • Vlastní implementace způsoby jako „počet znaků od začátku body elementu“, „počet znaků označeného textu“
  • Použití knihovny rangy.js (nad kterou se v době psaní tohoto článku stejně už 2 roky nic neděje)
  • Vlastní implementace způsobem serializace a deserializace Range objektu

Zatím jediný a funkční způsob byl vlastní implementace založená na znalosti označeného textu a počet výskytů jednotlivých slov, případně úseků slov – záleží na tom jak moc je DOM komplikovaný/velký/strukturovaný.

Working Effectively with Legacy Code – záznam, dema a slides [WUG Praha 04/2017]

Slides z mé přednášky pro WUG Praha z 26.4.2017:

Záznam z přednášky je publikován na našem HAVIT YouTube Channel.

Dotčená témata

  • „definice“ Legacy Code
  • Refactoring Mindset
  • Roslyn Code Analyzers – C#, StyleCop, SonarLint, Global Suppressions
  • Testability – extract dependencies do virtuálních metod + override v testu
  • Mocking – Moq
  • Advanced Testing – Fixture
  • extrakce dependencies, Dependency Injection

.NET C# – Parallel, Async – Martin Havel [HAVIT Vzdělávací okénko 6.4.2017]

Záznam a dema z příspěvku Martina Havla pro Vzdělávací okénko HAVIT z 6.4.2017:

Záznam je publikován na našem HAVIT YouTube Channel.

Dotčená témata

  • Thread.Start()
  • Task.Run()Task.Factory.StartNew()
  • Parallel.For()Parallel.Invoke()
  • awaitasync
  • ThreadPool.QueueUserWorkItem()
  • kvízy

Vzdělávací okénka máme v HAVITu každý čtvrtek odpoledne pro vzájemné obohacování v technologických dovednostech. Vybrané příspěvky budeme touto cestou publikovat, zvažujeme i umožnit účast na vzdělávacích akcích i příchozím z ulice. ;-)

TFS (XAML) Builds – Jak vytvořit lokální workspace

TFS (XAML) Build vytvoří workspace na build serveru, do něj stáhne zdrojové kódy a ty kompiluje. Workspace vytváří aktivita CreateWorkspace, která vytvoří serverový workspace a není způsob, jak této aktivitě říct, aby vytvořila workspace lokální. Aktivita CreateWorkspace navíc vrací instanci reprezentující založený workspace.

Když tedy nelze při říct, že založený workspace má být lokální, lze použít drobný workaround – založit workspace jako serverový a dodatečně jej přepnout na lokální. To lze provést zavoláním metody Update na instanci vrácené z CreateWorkspace. V rámci TFS (XAML) Buildu to můžeme udělat reflexí, tedy aktivitou InvokeMethod s těmito parametry:

  • Target Object: Workspace (resp. taková proměná, ve které máme vrácenou instanci z activity CreateWorkspace)
  • Method Name: Update
  • Parameters: In / Microsoft.TeamFoundation.VersionControl.Client.UpdateWorkspaceParameters / New UpdateWorkspaceParameters With { .Location = Microsoft.TeamFoundation.VersionControl.Common.WorkspaceLocation.Local }

O rozdílech mezi lokálním a serverovým workspacem si můžete přečíst tady nebo tady. Naše touha pro přechod ze serverového na lokální workspace byla vedena přiblížením prostředí buildu vývojářskému prostředí, zejména nenastavení read-only atributů na stažených souborech.

 

[ASP].NET Worst Practices – záznam, slides a dema [MS Fest Brno 03/2017]

Slides a dema z mé přednášky pro konferenci MS Fest Brno z 19.3.2017:

Záznam z přednášky je publikován na našem HAVIT YouTube Channel.

Dotčená témata

  • skládání stringů vs. StringBuilder
  • vyhledávání v datech – List vs. BinnarySearch vs. Dictionary vs. LINQ ToLookup()
  • ASP.NET Sessions
  • ASP.NET Over-posting / Mass-assignment
  • Nevěřte vstupu od klientů

ASP.NET Core – Dependency Injection & Unit-testing – záznam, slides a dema [ShowIT Bratislava 02/2017]

Slides a dema z mé přednášky pro konference G2B TechEd Brno z 7.2.2017 a ShowIT Bratislava z 8.2.2017:

Záznam z přednášky je publikován na našem HAVIT YouTube Channel.

Dotčená témata

  • Dependency Injection principy
  • Startup.cs: ConfigureServices(), ConfigureDevelopmentServices(), custom DI container
  • unit-testing principy
  • ukázka testování business-služby PriceManager

MSB3270 Processor Architecture Mismatch Warning – jak ho skrýt z výpisu chyb

Pokud máte stejně jako já rádi čisté okno Error List ve Visual Studiu, pak vás může otravovat warrning typu

There was a mismatch between the processor architecture of the project being built „MSIL“ and the processor architecture of the reference „XYZ, Version=XXX, Culture=neutral, processorArchitecture=x86“, „XXX“. This mismatch may cause runtime failures. Please consider changing the targeted processor architecture of your project through the Configuration Manager so as to align the processor architectures between your project and references, or take a dependency on references with a processor architecture that matches the targeted processor architecture of your project.

Nechci teď rozebírat, jak vyřešit příčinu hlášky, ale mám návod, jak se vypořádat se situací, kdy se příčiny zbavit nechcete/nemůžete, ale potřebujete hlášku potlačit (suppress).

Do .csproj souboru příslušného projektu stačí do úvodní sekce PropertyGroup přidat:

<PropertyGroup>
  <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
</PropertyGroup>

Jenom pozor, že tím můžete zakrýt i další varování, která se objeví později a mohou vás zajímat.

C# 7.0 – Rychlý přehled novinek

Out variables

Možnost deklarovat out-proměnné inline při volání metod:

// dříve
int x;
if (int.TryParse(s, out x)) ...

// C# 7.0
if (int.TryParse(s, out int x)) ...

// C# 7.0
if (int.TryParse(s, out var x)) ...

…rozhodně přehlednější zápis.

Tuples

Možnost vytvářet lehké datové struktury s několika fieldy a používat je i jako návratové hodnoty metod:

private static (int Max, int Min) Range(IEnumerable<int> numbers)
{
    int min = int.MaxValue;
    int max = int.MinValue;
    foreach(var n in numbers)
    {
        min = (n < min) ? n : min;
        max = (n > max) ? n : max;
    }
    return (max, min);
}

var result = Range(numbers);
Console.Write($"{result.Min} - {result.Max}");

(int max, int min) = Range(numbers); // deconstruction

Bude zneužíváno lenochy, co nechtějí psát malé ViewModely, ale budou raději riskovat zmatky při deconstruction (záleží na pořadí prvků, ne názvech!).

Lokální funkce

Možnost definovat pomocné lokální funkce uvnitř metod:

public IEnumerable<MyClass> GetActiveItems()
{
    var rawData = myClassDataService.GetPrimaryItems().Where(i => IsActive(i));
    if (!rawData.Any())
    {
        return myClassDataService.GetSecondaryItems().Where(i => IsActive(i));
    }
    return rawData;

    bool IsActive(MyClass item)
    {
        return (item.State == States.Active) && (item.Rows.Any());
    }
}

Dosud bylo možné použít anonymní lambda výrazy, syntaxe je však nyní trochu přívětivější. Zde například chci 2x vyhodnotit stejnou podmínku IsActive, ale z jiné metody ve třídě to nepotřebuji a lokální funkce je tak pro mě přehlednější, než samostatná privátní metoda.

Pattern matching

Možnost pomocí klíčových slov is a switch řídit větvení kódu, popř. s možností dodatečných podmínek when:

public static int SumTree(IEnumerable<object> values)
{
    var sum = 0;
    foreach(var item in values)
    {
        if (item is int val)
            sum += val; // leaf-node
        else if (item is IEnumerable<object> subList)
            sum += SumTree(subList);
    }
    return sum;
}
public static void SwitchPattern(object o)
{
    switch (o)
    {
        case null:
            Console.WriteLine("pattern pro konstantu");
            break;
        case int i:
            Console.WriteLine("Je to číslo!");
            break;
        case Person p when p.FirstName.StartsWith("Ka"):
            Console.WriteLine($"Jméno začíná na Ka: {p.FirstName}");
            break;
        case Person p:
            Console.WriteLine($"Jiné jméno: {p.FirstName}");
            break;
        case var x:
            Console.WriteLine($"jiný typ: {x?.GetType().Name} ");
            break;
        default:
            break;
    }
}

Je to podobné, jako C# 6 přišel s podmínkami u catch-bloků. Při if-is testech na typ se bude hodit.

Ref locals and returns

Možnost použít referenci na proměnnou definovanou jinde:

public ref int Find(int number, int[] numbers)
{
    for (int i = 0; i < numbers.Length; i++)
    {
        if (numbers[i] == number) 
        {
            return ref numbers[i]; // vrať referenci, nikoliv hodnotu
        }
    }
    throw new IndexOutOfRangeException($"{nameof(number)} nenalezeno");
}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // vrátí referenci na pozici se sedmičkou
place = 9; // nahradí v poli nalezenou sedimičku devítkou
WriteLine(array[4]); // vypíše 9

Využití je celkem specifické, ale výjimečně se může hodit.

Další expression-bodied members

// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}

…snad jedině u jednoduchých properties to zlepšuje čitelnost, jinak nejsem moc velkým příznivcem expression-bodies members.

Vyhazování výjimek z výrazů

Hodí se třeba v kombinaci s exression-bodied members:

public string Name
{
    get => name;
    set => name = value ?? throw new ArgumentNullException();
}

ValueTask pro async

Task<T> je referenční datový typ a jeho použití znamená alokaci objektu na haldě. Pro hodnotové návratové typy nově přichází optimalizace v podobě ValueTask<T>

public async ValueTask<int> Func()
{
    await Task.Delay(100);
    return 5;
}

Čitelnější možnosti zápisu číselných konstant

public const int One =  0b0001; // binární číslo
public const int Sixteen =   0b0001_0000; // oddělovač (lze použít kdekoliv, ignoruje se)
public const long HodneMoc = 100_000_000_000;
public const double AvogadroConstant = 6.022_140_857_747_474e23;

…binární zápis se může hodit na flagy, oddělovač na velká čísla.

ASP.NET MVC Razor: Renderování stringů do JavaScriptu

Pokud se budete pokoušet v Razor-View vytvořit JavaScript přiřazením textových hodnot, můžete si pěkně naběhnout:

var name = '@Model.Name';

Pokud totiž v Model.Name bude nějaká záludnější hodnota, dostanete například:

var name = 'Bož&amp;#237;čku kolekcička&amp;#39;\'  // original: Božíčku kolekcička'\

Taky možná najdete, že existuje HttpUtility.JavaScriptStringEncode() a zkusíte:

var name = '@HttpUtility.JavaScriptStringEncode(Model.Name)';

…což taky nedopadne nejlépe:

var name = 'Bož&amp;#237;čku kolekcička\u0027\\'  // original: Božíčku kolekcička'\

Až nakonec přijdete na to, že je potřeba:

var name = '@Html.Raw(HttpUtility.JavaScriptStringEncode(Model.Name))';

…abyste dostali:

var name = 'Božíčku kolekcička\u0027\\'  // original: Božíčku kolekcička'\

Případně použijete variantu, která si uvozovky (ne apostrofy) doplní sama.

var name = @Html.Raw(Json.Encode(Model.Name));

…z čehož plyne poučení, že se takto string do HTML raději nepokoušejte vůbec emitovat a raději přenášejte JSON objekty.

VS2017 #NoResharper Challenge: Navigace Ctrl+T, [něco]+T

Klíčovou funkcí Resharperu pro mě byla rychlá navigace k souborům – vyhledávání podle názvu souboru, třídy, memberu, atp. Moje nejčastější klávesová zkratka byla Shift+Alt+T, hledání souboru.

Visual Studio mělo už v předchozích verzích funkci Navigate to… pod klávesovou zkratkou Ctrl+,. Alternativou bylo i Ctrl+ů – hledání v Solution Exploreru. Ani jedna z těchto variant však kvalitativně ani zdaleka nesahala Resharperu po paty.

Visual Studio 2017 udělalo v tomto značný krok kupředu. Původní Navigate to… se proměnilo v nové Go to… (kromě Ctrl+, přibyla i klávesová zkratka Ctrl+T) a umožňuje už samostatné hledání v souborech/memberech/…

image

Kdo je zvyklý z Resharperu, syntaxe je zde trochu jiná a je to o zvyk. Kromě klávesových zkratek přímo na jednotlivé typy vyhledávání lze použít i Go To All… (Ctrl+T) a přepínač v podobě prvního znaku vyhledávací fráze:

image

Největší slabinou zatím zůstává, že výsledky vyhledávání nejsou seřazeny podle relevance, ale podle abecedy. Občas je to tedy porod:

image

…taková blbost, ale otráví to, když ImportedBook.cs najdete až na třetí stránce a neexistuje syntaxe, jak to dopravit nahoru, dokud existují odpovídající soubory (nebo jsem alespoň zatím nenašel, jak to ochočit). Až na tyto výjimky se s tím však zdá se žít dá, byť plnohodnotná náhrada Resharperu to není.

Kdo by se s tím nesžil, toho odkážu na alternativní doplňky, které jsem zkoušel při své VS2015 #NoResharper challenge: