Táto stránka pre svoju správnu funkčnosť vyžaduje súbory cookies. Slúžia na authentifikáciu návštevníka, analýzu návštevnosti a reklamnú personalizáciu.
logo
Prihlásenie / Registrácia
mobile

Zavrieť
 

Custom Visualizer pre Visual Studio

Visual Studio nám ponúka množstvo pomocných nástrojov, ktoré nám uľahčujú prácu. Jednou z nich sú Visualizer-i vybraných typov objektov, ktoré vedia byť veľmi užitočné pri ladení našich aplikácii. V nasledujúcom článku si jeden z nich vytvoríme.

Čo je to ten Visualizer?

String Visualizer
Visualizer zabudovaný vo Visual Studio-u je nástroj, ktorý využívame denne a pomáha nám s ladením našich aplikácii.

Ak ste sa s týmto pojmom ešte nestretli, zapnite si VS, napíšte jednoduchý program s premennou typu String a nastavte breakpoint na nasledujúci riadok po jej naplnení hodnotou. Pri zastavení programu na našom breakpoint-e prejdime myšou nad premennou a zobrazí sa nám "bublina", ktorá okrem iného bude obsahovať aj ikonu lupy. Po kliku na lupu sa zobrazí obsah premennej v novom okne. Toto okno je Text Visualizer. Možno to na prvý pohľad môže vyzerať ako zbytočnosť, no predstavte si, že by naša premenná obsahovala napr. 50 riadkový reťazec, html kód alebo xml a hneď to začne dávať iný zmysel. Vedľa ikony lupy môže byť zobrazená ikona šípky (ako v combobox-e), ktorá sa zobrazuje v prípade, ak je pre daný typ k dispozícii viac visualizer-ov (V našom prípade Text, XML, HTML a JSON Visualizer) a dovoľuje nám zvoliť, ktorý z nich chceme použiť.

Samozrejme to pri premennej typu String nekončí. Veľmi užitočným je aj DataSet/DataTable Visualizer, ktorý vie zobraziť dáta v DataSet alebo DataTable vo forme gridu. A nekončí to ani len pri štandardných .NET-ových typoch... VS nám umožňuje vytvoriť si aj vlastný Visualizer pre (takmer) akýkoľvek objekt aký potrebujeme, vrátane aj našich vlastných a to bude náplňou tohto článku.

Testovací projekt

Najlepšie si celú "problematiku" ukážeme na príklade. Predstavme si, že robíme softvér na prípravu rezných plánov pre nejaký rezací stroj (ďalej len cutter), ktorého súčasťou je aj optimalizácia rezacieho plánu s cieľom minimalizovať odpad a čo najoptimálnejšie vyplniť plochu rezaného materiálu požadovanými dielcami / odrezkami. Pri tvorení a následnom ladení tejto funkčnosti budeme potrebovať dobrú predstavivosť na "vizualizáciu" rezacieho plánu v hlave, aby sme vedeli postupne vychytať všetky jeho muchy.

A tu prichádza chvíľa pre visualizer na objekt, v ktorom máme uložený aktuálny stav páliaceho plánu v procese spracovania, prípadne aj jeho finálny stav. Samozrejme, môžeme si aktuálny stav páliacieho plánu zobrazovať aj v nejakom formulári aplikácie pod ktorým ho vidí aj používateľ aplikácie, no museli by sme zakaždým prerušiť ladenie aplikácie, čo bude zdržovať. Visualizer beží počas ladenia a je možného ho vyvolať opakovane, napríklad po drobnej zmene dát a pod.

Samotný visualizer sa skladá z dvoch častí. Prvá časť beží na úrovni debuggera (debugger side) a druhá na úrovni ladeného procesu (debuggee side). Na strane debuggera sa visualizer stará o spracovanie dát a ich spracovanie do vizuálnej formy a na strane ladeného procesu pripravuje samotné dáta.

Začneme vytvoreným nového projektu typu Class library s názvom Cutter. Zmeníme názov Solution na CustomVisualizer. Do projektu našej knižnice pridáme referenciu na System.Drawing, súbor so zdrojovým kódom, ktorý nám vytvorilo VS premenujeme na CutterPlan.cs a pridáme do neho nasledujúci kód

Zdrojový kód:
using System;
using System.Drawing;

namespace Cutter
{
    [Serializable]
    public class CutterPlan
    {
        public string Name { get; }
        public CutterItem[] Items { get; }
        public Rectangle Plate { get; }

        public CutterPlan(string Name, Rectangle Plate, CutterItem[] Items)
        {
            this.Name = Name;
            this.Items = Items;
            this.Plate = Plate;
        }
    }

    [Serializable]
    public class CutterItem
    {
        public Rectangle ItemRect { get; }
        public Color ItemColor { get; }

        public CutterItem(Rectangle ItemRect, Color ItemColor)
        {
            this.ItemRect = ItemRect;
            this.ItemColor = ItemColor;
        }
    }
}

Týmto sme si zadefinovali triedu CutterPlan, ktorá bude obsahovať dáta rezacieho plánu a pre ktorú budeme vytvárať náš visualizer. Kód zároveň obsahuje triedu CutterItem, ktorá bude reprezentovať jednotlivý vyrezaný diel. Určite Vám udrelo do očí, že obe triedy sú označené ako Serializable, čo predpoklad toho, aby sme daný objekt vedeli preniesť z debuggee na debugger side. Pri odovzdávaní dát medzi jednotlivými časťami visualizer sú dáta prenášané v "serializovanej forme". Ak je teda objekt sám o sebe "serializovateľný" zjednodušuje nám to prácu.

Do našej knižnice pridáme ešte súbor s názvom CutterInfo.cs.

Zdrojový kód:
namespace Cutter
{
    public class CutterInfo
    {
        public string Vendor { get; }
        public string Model { get; }
        public int Year { get; }

        public CutterInfo(string Vendor, string Model, int Year)
        {
            this.Vendor = Vendor;
            this.Model = Model;
            this.Year = Year;
        }
    }
}

Pridanie referencie na iný projekt v Solution
Táto trieda sa tvári, že bude obsahovať informácie o našom rezacom stroji. Tentokrát sme ju neoznačili ako Serializable (aj keď nám v tom nič nebráni) a bude to testovacia trieda pre visualizer na Non-Serializable triedu.

Pokračujeme pridaním testovacej aplikácie do našej Solution typu Console application s názvom TestApp. Do aplikácie pridáme referencie na System.Drawing a našej knižnice Cutter, ktorú sme si nedávno vytvorili.

Obsah súboru Program.cs upravíme nasledovne:

Zdrojový kód:
using Cutter;
using System.Drawing;

namespace TestApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Rectangle Plate = new Rectangle(0, 0, 500, 250);

            CutterItem[] Items =
            {
                new CutterItem(new Rectangle(0,0,100,100), Color.Red),
                new CutterItem(new Rectangle(0,102,120,120), Color.Blue),
                new CutterItem(new Rectangle(102,0,20,100), Color.Green)
            };


            CutterPlan Plan = new CutterPlan("Test CP", Plate, Items);
            CutterInfo ci = new CutterInfo("Codeblog", "CB001", 2020);
        }
    }
}

V kóde aplikácie sme si vytvorili testovacie objekty typu CutterPlan a CutterInfo, na ktorých si otestujeme naše visualizeri.

Visualizer pre Serializable typy

Referencia na extensions
Do našej solution pridáme ďalší projekt typu Class library a nazveme ho CutterDataVisualizer. Do projektu pridáme dva nové súbory pre triedy s názvami DebuggeeSide.cs a DebuggerSide.cs a nový formulár CutterDataVisualizerForm. Rovnako ako do testovacej aplikácie pridáme referenciu na našu knižnicu Cutter a navyše budeme potrebovať referenciu na Microsoft.VisualStudio.DebuggerVisualizers, ktorú vieme nájsť v časti Assemblies a Extensions. Táto referencia nám zaručí prístup k triedam, ktoré potrebujeme na vytvorenie visualizera.

Ako prvý si vyplníme súbor DebuggeeSide.cs.

Zdrojový kód:
using System.IO;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace CutterDataVisualizer
{
    public class DebuggeeSide: VisualizerObjectSource
    {
        public override void GetData(object target, Stream outgoingData)
        {
            base.GetData(target, outgoingData);
        }
    }
}

Aj keď samotný kód veľa funkčnosti nemá, poslúži nám ako ukážka na vysvetlenie. Trieda, ktorá má pripraviť dáta pre visualizer musí byť odvodená od VisualizerObjectSource z namespace Microsoft.VisualStudio.DebuggerVisualizers. V našom prípade je pre nás podstatná metóda GetData, ktorá je volaná v prípade potreby prípravy dát. Dáta, ktoré do nej vstupujú sú pre nás momentálne relevatné, čiže do nich nebudeme zasahovať.

Pokračujeme súborom DebuggerSide.cs.

Zdrojový kód:
using Cutter;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace CutterDataVisualizer
{
    public class DebuggerSide : DialogDebuggerVisualizer
    {
        protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            object InputData = objectProvider.GetObject();

            if (InputData != null && InputData is CutterPlan)
            {
                CutterDataVisualizerForm VizForm = new CutterDataVisualizerForm();
                VizForm.LoadData((CutterPlan)InputData);
                windowService.ShowDialog(VizForm);
            }
        }
    }
}


Trieda DebuggerSide vycháza z DialogDebuggerVisualizer a preťažuje metódu Show, ktorá je volaná pri požiadavke o zobrazenie visualizeru.

Z parametra objectProvider zavoláme metódu GetObject, ktorá by nám mala vrátit objekt, ktorý je potrebné "zobraziť". V prípade, ak dáta pre visualizer majú príst ako Stream vieme namiesto metódy GetObject použiť GetData.

Ďalej si overíme aký objekt nám prišiel a vytvoríme objekt nášho formulára (za moment si ho pripravíme), naplníme ho dátami a pomocou metódy windowService.ShowDialog ho zobrazíme.

Predposledným krokom bude vytvorenie "vizualizačného formulára". Samotný formulár sme si do projektu pridali hneď v úvode tejto časti článku, teraz si ho doplníme o požadované funkčnosti.

Začnime pridaním vizuálnych komponent na formulár.
Do ľavého horného rohu pridáme Label (nazveme ho lbl_name) s textom "Name", vedľa neho TextBox (tb_name), ktorý nastavíme ako readonly a budeme v ňom zobrazovať vlastnosť Name z objektu typu Cutter.

Pod nich umiestnime Panel (pnl_main), ktorým vyplníme takmer celý zvyšok formulara (v spodnej časti necháme miesto na button) a do ktorého vložíme PictureBox (pb_data). PictureBox umiestnime do ľavého horného rohu panelu a nastavíme mu vlastnosti SizeMode na AutoSize.

Samotnému Panelu nastavíme tieto vlastnosti:
Anchor = Top, Bottom, Left, Right - čím zabezpečíme, aby sa panel prispôsoboval veľkosti formulára.
AutoScroll = True - V prípade, ak rozmery pb_data presiahnu rozmery panelu, dôjde k aktivovaniu ScrollBarov na paneli.
BorderStyle = FixedSingle

Pod panel k pravému dolnému rohu formulára umiestnime už spomenutý button (btn_close) s textom "Close" a upravíme vlastnosť Anchor = Bottom, Right

Samotný výkonný kód formulára pozostáva z dvoch metód:

Zdrojový kód:
...
public void LoadData(CutterPlan Input)
{
    tb_name.Text = Input.Name;

    Bitmap ImgSrc = new Bitmap(Input.Plate.Width, Input.Plate.Height);
    Graphics Canvas = Graphics.FromImage(ImgSrc);
    Canvas.FillRectangle(Brushes.White, Input.Plate);

    foreach(CutterItem Item in Input.Items)
    {
        Canvas.FillRectangle(new SolidBrush(Item.ItemColor), Item.ItemRect);
    }

    pb_data.Image = ImgSrc;
}

private void btn_close_Click(object sender, EventArgs e)
{
    this.Close();
}
...

Metóda LoadData zobrazí názov páliaceho plánu do TextBoxu tb_name, vytvorí sa premenná ImgSrc typu Bitmap, na ktorú jednotlivé odrezky nakreslíme a nakoniec zobrazíme v pb_data.

btn_close_Click je event handler kliku button btn_close, ktorý samozrejme musí byť priradení k eventu cez okno Properties.

Ostáva nám už len do projektu pridať informáciu pre VS Debugger, že je náš projekt (resp DLL ktoré vznikne skompilovaním) je visualizer.
Otvoríme si teda súbor AssemblyInfo.cs, ktorý nájdete v Solution Exploreri v časti Properties (pozor, nie v okne Properties) a na koniec súboru pridáme nasledovný kód:

Zdrojový kód:
[assembly: System.Diagnostics.DebuggerVisualizer(typeof(DebuggerSide),typeof(DebuggeeSide),Target = typeof(CutterPlan),Description = "Cutter data visualizer")]

Visualizer je k dispozícii
CutterData Visualizer
Prvý parameter hovorí o triede, ktorá reprezentuje visualizer, druhý je informácia o triede na prípravu dát, tretí hovorí o type, pre ktorý je náš visualizer určený a posledným je popis našeho projektu.

Týmto je náš visualizer pripravený, môžeme ho skompilovať (projekt CutterDataVisualizer) a všetky vytvorené knižnice skopírujeme do priečinka {Moje dokumenty}\Visual Studio XXXX\Visualizers, kde XXXX je číslo verzie VS.

Znovuotvoríme našu Solution, otvoríme súbor Program.cs v projekte TestApp, umiestnime breakpoint na koniec metódy Main a testovaciu aplikáciu spustíme.

Ak sme postupovali správne, tak pri zastavený programu na breakpointe a prejdení myšou cez premennú Plan by mal byt náš visualizer prístupný a môžeme ho zobraziť.

Visualizer pre Non-Serializable typy

Samozrejme, nie vždy je všetko tak jednoduché ako čakáme...
Vedieť urobiť visualizer pre Serializable typ je síce pekné, no v praxi sa stretneme aj s triedami, ktoré proste Serializable nie sú a to čo sme si v tomto článku ukázali, nám proste nebude stačiť.Vytvoriť visualizer pre Non-Serializable typy je samozrejme možné, len je to o trochu komplikovanejšie.

Dôležité je uvedomiť si nasledovné:
1. Visualizer nezobrazuje úplne všetky dáta objektu, zobrazuje len tie, ktoré používateľ skutočne potrebuje. Čiže prenášať celý objekt vlastne ani nepotrebujeme.
2. Nič nám nebráni, aby sme si z objektu potrebné dáta preniesli pomocou nejakého typu, ktorý bude Serializable.

Takže technické riešenie nášho problému bude pozostávať z:
1. Zadefinujeme si triedu, ktorá bude Serializable a bude schopná preniesť nami požadované dáta.
2. V triede, ktorá pripravuje dáta pre visualizer si zo zdrojového objektu zoberieme potrebné údaje, vložíme ich do nami definovaného objektu na prenos a predáme ho ďalej.
3. Pri spracovávaní dát na strane debuggera nebudeme očakávať zdrojový objekt, ale nami vytvorený prenosový objekt a dáta si zoberieme z neho.

Toľko teórie, pustime sa do kódu...

Vytvoríme si ďalší projekt typu Class library a nazveme ho CutterInfoVisualizer. Ako už názov napovedá, budeme visualizovať triedu CutterInfo, ktorú sme si zadefinovali v úvode tohto článku.

Vytvoríme si v ňom súbory DebuggeeSide.cs, DebuggerSide.cs a CutterInfoSerializable.cs.

Začnime súborom CutterInfoSerializable.cs, v ktorom si zadefinujeme triedu na prenos dát:

Zdrojový kód:
using System;
using Cutter;

namespace CutterInfoVisualizer
{
    [Serializable]
    public class CutterInfoSerializable
    {
        public string Vendor { get; }
        public string Model { get; }
        public int Year { get; }

        public CutterInfoSerializable(CutterInfo Source)
        {
            this.Vendor = Source.Vendor;
            this.Model = Source.Model;
            this.Year = Source.Year;
        }
    }
}

Trieda je technicky totožná s CutterInfo, no budeme sa tváriť, že CutterInfo obsahuje aj niečo, čo mu bráni, aby bol Serializable. Konštruktor má jeden parameter typu CutterInfo, s ktorého si vyberie dáta, ktoré potrebuje.

Pokračujeme súborom DebuggeeSide.cs

Zdrojový kód:
using System.IO;
using Cutter;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace CutterInfoVisualizer
{
    public class DebuggeeSide: VisualizerObjectSource
    {
        public override void GetData(object target, Stream outgoingData)
        {
            if (target != null && target is CutterInfo)
            {
                CutterInfoSerializable SerializableTarget = new CutterInfoSerializable((CutterInfo)target);
                base.GetData(SerializableTarget, outgoingData);
            }
        }
    }
}

V kóde vidíme využitie našej prenosovej triedy na spracovanie zdrojového objektu a prenos dát.

A do tretice DebuggerSide.cs

Zdrojový kód:
using Cutter;
using Microsoft.VisualStudio.DebuggerVisualizers;

namespace CutterInfoVisualizer
{
    public class DebuggerSide : DialogDebuggerVisualizer
    {
        protected override void Show(IDialogVisualizerService windowService, IVisualizerObjectProvider objectProvider)
        {
            object InputData = objectProvider.GetObject();

            if (InputData != null && InputData is CutterInfoSerializable)
            {
                CutterInfoVisualizerForm VizForm = new CutterInfoVisualizerForm();
                VizForm.LoadData((CutterInfoSerializable)InputData);
                windowService.ShowDialog(VizForm);
            }
        }
    }
}

Význam kódu je obdobný ako v CutterDataVisualizer, preto ho bližie popisovať netreba.

Otvoríme si aj AssemblyInfo.cs a na koniec súboru pridáme:

Zdrojový kód:
[assembly: System.Diagnostics.DebuggerVisualizer(typeof(DebuggerSide), typeof(DebuggeeSide), Target = typeof(CutterInfo), Description = "Cutter info visualizer")]

Formulár visualizer-a CutterInfo

A ostáva nám už len vytvoriť formulár.

Do projektu pridáme nový formular s menom CutterInfoVisualizerForm, na ktorý pridáme 6x Label a jeden Button. Labely pomenujeme lbl_vendor, lbl_vendorval, lbl_model, lbl_modelval, lbl_year a lbl_yearval. Ako názvy napovedajú, jedná sa o dvojičky, ktoré umiestnime k sebe. Ten, ktorého názov konči na val slúži na zobrazenie hodnoty a jeho dvojička slúži ako popisok. Button pomenujeme btn_close a bude nám formulár zatvárať.

Výkonný kód bude nasledovný:

Zdrojový kód:
public void LoadData(CutterInfoSerializable Input)
{
    lbl_modelval.Text = Input.Model;
    lbl_vendorval.Text = Input.Vendor;
    lbl_yearval.Text = Convert.ToString(Input.Year);
}

private void btn_close_Click(object sender, EventArgs e)
{
    this.Close();
}

Projekt CutterInfoVisualizer môžeme skompilovať a vyskúšať v projekte TestApp na premennej ci, typu CutterInfo.

Debugging visualizer-u

Nami vytvorené visualizeri sú relatívne jednoduché, no ak budeme vyvíjať nejaké komplexnejšie riešenia, skôr či neskôr dôjdeme do bodu, že budeme náš kód potrebovat debuggovať.

Samozrejme aj na toto VS myslí a poskytuje nám na to prostriedky. Konkrétne sa jedná o triedu VisualizerDevelopmentHost v namespace Microsoft.VisualStudio.DebuggerVisualizers. Pomocou nej vieme nasimulovať spustenie visualizera priamo z kódu. Do oboch tried DebuggerSide v našich projektoch pridáme statické metódy TestVisualizer, ktoré v prípade potreby budeme volať z kódu a ako argument použijeme objekt, ktorý chceme vizualizovať.

CutterDataVisualizer

Zdrojový kód:
...
public static void TestVisualizer(object objectToVisualize)
{
    if (objectToVisualize != null && objectToVisualize is CutterPlan)
    {
        VisualizerDevelopmentHost myHost = new VisualizerDevelopmentHost(objectToVisualize, typeof(DebuggerSide));
        myHost.ShowVisualizer();
    }
}
...

CutterInfoVisualizer

Zdrojový kód:
...
public static void TestVisualizer(object objectToVisualize)
{
    if (objectToVisualize != null && objectToVisualize is CutterInfo)
    {
        CutterInfoSerializable Serializable = new CutterInfoSerializable((CutterInfo)objectToVisualize);
        VisualizerDevelopmentHost myHost = new VisualizerDevelopmentHost(Serializable, typeof(DebuggerSide));
        myHost.ShowVisualizer();
    }
}
...

V kóde pre projekt CutterInfoVisualizer vidíme, že táto metóda musí suplovat aj funkčnosť DebugeeSide. Pre samotné testovanie visualizeru musíme jeho DLL pridať do referencií projektu a následneho ho vieme zavolať:

Zdrojový kód:
CutterDataVisualizer.TestVisualizer([objekt]);

alebo

Zdrojový kód:
CutterInfoVisualizer.TestVisualizer([objekt]);

Upozornenie na na záver:
V prípade, ak máme v testovacej aplikácii priamo nalinkované DLL visualizerov a zároveň sú aj nainštalované vo VS, môže byť ich volanie cez debugger vo VS blokované.
Zároveň odporúčam v nastaveniach projektu visualizera vypnúť možnost Optimize code

Záverom

Dúfam že bol tento článok dostatočne zrozumiteľný, prípade nejakých nejasností alebo ak ste našli chybu neváhajte nechať komentár pod článkom (po registrácii).

Kompletný zdrojový kód nami vytvorenej Solution môžete sťahovať z > link <

Použité zdroje

Visualizer Architecture - docs.microsoft.com > link <
How to: Test and Debug a Visualizer - docs.microsoft.com > link <
Writing a Custom Debugger Visualizer for Visual Studio - wrightfully.com > link <

Codeblog
Diskusia

Žiadne príspevky v diskusii.

Nový príspevok

Na prispievanie do diskusie musíte byť prihlásený.