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
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ódZdrojový 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.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;
}
}
}
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;
}
}
}
{
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
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.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);
}
}
}
Visualizer pre Serializable typy
Referencia na extensions
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.using Microsoft.VisualStudio.DebuggerVisualizers;namespace CutterDataVisualizer
{
public class DebuggeeSide: VisualizerObjectSource
{
public override void GetData(object target, Stream outgoingData)
{
base.GetData(target, outgoingData);
}
}
}
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);
}
}
}
}
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 = FixedSinglePod panel k pravému dolnému rohu formulára umiestnime už spomenutý button (btn_close) s textom "Close" a upravíme vlastnosť Anchor = Bottom, RightSamotný 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. 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();
}
...
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
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.csusing 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;
}
}
}
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.csusing 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);
}
}
}
}
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: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);
}
}
}
}
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.{
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();
}
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ť.CutterDataVisualizerZdrojový kód:
...
public static void TestVisualizer(object objectToVisualize)
{
if (objectToVisualize != null && objectToVisualize is CutterPlan)
{
VisualizerDevelopmentHost myHost = new VisualizerDevelopmentHost(objectToVisualize, typeof(DebuggerSide));
myHost.ShowVisualizer();
}
}
...
CutterInfoVisualizerpublic static void TestVisualizer(object objectToVisualize)
{
if (objectToVisualize != null && objectToVisualize is CutterPlan)
{
VisualizerDevelopmentHost myHost = new VisualizerDevelopmentHost(objectToVisualize, typeof(DebuggerSide));
myHost.ShowVisualizer();
}
}
...
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ť: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();
}
}
...
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 <
Žiadne príspevky v diskusii.
Na prispievanie do diskusie musíte byť prihlásený.