sigsys|dk


Blog om udvikling

ASP.NET og jquery - find panelet;

udgivet 7/3/2015 4:11:04 PM

En panel i asp.net bliver expandet til en div. Vil du have fat i denne div via id’et, kan du ikke bare tilgå den ved at skrive:

$('#PanelId')

da en id’et på panelet bliver sammensat af flere forskellige faktorer, fx. dens parent osv. For at bruge overstående er man derfor nød til at kende navngivningsreglerne, eller se på kildekoden som IIS’en smider tilbage i hovedet på browseren, dog kan man med jquery gøre noget meget smart. Man kan nemlig søge DOM’en efter div id’er indeholdende en tekst, meget lig SQL’s LIKE statement.

Har vi defineret en panel sådan her:

<asp:Panel ID="panelInfoBox" Style="border: 1px solid black;" runat="server"></asp:Panel>

Kan vi få fat i fx teksten, ved at bruge lidt jquery magi:

$("div[id$=panelInfoBox]").text("hello world"); 

Her sætter vi teksten i panelet. Dette er meget nemmere at læse, og virker hvis man omrokerer hele html’en.

Mono.Cecil - part 2;

udgivet 7/3/2015 11:03:06 AM

Så fik jeg tid til at skrive lidt om Mono.Cecil igen. Sidst skrev jeg hvordan man lavede en reference til en anden assembly, og skrev den “modificerede” assembly, med den nye reference, ned på disken. Dog er der ikke meget ved kun at lave referencer til andre assemblies, dette bliver lidt kedelig i længden. Jeg vil i dette indlæg forklare hvordan man kan kalde metoderne i den nylige referencerede assembly.

For at kunne lave et sådanne hack generelt, hvor injecter metode kald ind i andre assemblies, kræves der at man har lidt styr på de mere lavtstående dele af C#, nemlig IL kode. Dog er det minimalt hvor meget der bliver brugt i dette indlæg, så det er lige til overkomme, men vil man lave noget mere avanceret så vil jeg anbefale at man nærstudere MSIL lidt mere. Hvis du vil læse mere om MSIL opkoderne som bliver brugt længere nede, før du læser videre, så kan du tage et kig på

  1. call
  2. ldstr

Disse to opkoder bliver injectet via Mono.Cecils ILProcessor. Jeg har ikke kunne finde noget dokumentation på hvordan man bruger ILProcessoren, da Mono.Cecils dokumentation er skrevet til en ældre version end den seneste version af Mono.Cecil. Så den eneste dokumentation er her. Hvilket vil sige er lidt af en by i Rusland, hvis man gerne vil have lidt inspiration til hvad det kan bruges til Smiley. Dog kan jeg gå så langt og sige at man nemt kan få skabt sig et overblik via førnævnte FAQ, og lidt tid i tænkeboksen.

Jeg vil bygge lidt videre på min sidste artikel, hvor jeg har udvidet koden lidt,

Mono.Cecil.TypeDefinition type = lib.MainModule.Types.Where(x => x.Name == "Writer").Single();
Mono.Cecil.MethodDefinition method = type.Methods.Where(x => x.Name == "WriteLine").Single();

Mono.Cecil.MethodReference reference = source.MainModule.Import(method);

Mono.Cecil.TypeDefinition globalType = source.MainModule.Types.Where(x => x.Name == "Global").Single();
Mono.Cecil.MethodDefinition applicaionErrorMethod = globalType.Methods.Where(x => x.Name == "Application_Error").Single();

Mono.Cecil.Cil.ILProcessor processor = applicaionErrorMethod.Body.GetILProcessor();
Mono.Cecil.Cil.Instruction call = processor.Create(Mono.Cecil.Cil.OpCodes.Call, reference);
Mono.Cecil.Cil.Instruction str = processor.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"h");

processor.InsertBefore(applicaionErrorMethod.Body.Instructions[0], str);
processor.InsertAfter(applicaionErrorMethod.Body.Instructions[0], call);

Koden er faktisk lige til, dog har jeg brugt lang tid på at skrive den, da jeg ikke synes at der er meget dokumentation omkring på nettet, som fortæller hvordan man injecter et funktionskald. De fleste af de små snippets/spørgsmål/svar som flyder omkring på nettet, beskriver hvordan man gør dette med de ældre versioner af Mono.Cecil, som gør brug af en CilWorker til at injecte med. Denne metode er blevet byttet ud med førnævnte IlProcessor. Dette er faktisk den største forskel. Dog ligger der ikke et samlet eksempel på nettet på hvordan man laver et inject til en ekstern defineret metode. Jeg vil faktisk våge og påstå at min er det eneste fulde eksempel!! Hvilket jeg synes er lidt ærgeligt, da det er lidt sjovt at sidde og nørkle med.

Det der tog længst tid for mig at regne ud, var hvordan man brugte den injectede assembly reference. For man skulle ved første øjekast tro at man “bare” importerede den fundne metode og efterfølgende brugte den fundne metode i selve ILProcessoren:

...
Mono.Cecil.MethodDefinition method = type.Methods.Where(x => x.Name == "WriteLine").Single();

source.MainModule.Import(method);
...
Mono.Cecil.Cil.ILProcessor processor = applicaionErrorMethod.Body.GetILProcessor();
Mono.Cecil.Cil.Instruction call = processor.Create(Mono.Cecil.Cil.OpCodes.Call, method);
Mono.Cecil.Cil.Instruction str = processor.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"h");

processor.InsertBefore(applicaionErrorMethod.Body.Instructions[0], str);
processor.InsertAfter(applicaionErrorMethod.Body.Instructions[0], call);

Dog skal man bruge den returnerede “Mono.Cecil.MethodReference reference = …” videre i forløbet, ellers ved Mono.Cecil ikke hvad for assembly metoden referer til. Det jeg gjorde i første artikel, var altså bare en side af injectningen, hvor man fandt og tilknyttede referencen til den assembly som man vil modificere. Dog skal man bruge den returnerede reference, for at kompileren ikke brokker sig. Dejligt med compiletime tjek!

Det der sker i koden er at man først finder den metode som man vil injecte

Mono.Cecil.TypeDefinition type = lib.MainModule.Types.Where(x => x.Name == "Writer").Single();
Mono.Cecil.MethodDefinition method = type.Methods.Where(x => x.Name == "WriteLine").Single();

Mono.Cecil.MethodReference reference = source.MainModule.Import(method);

Her finder jeg altså metoden “WriteLine” i den statiske klasse “Writer”. Denne MetodeDefinition bruger jeg til at Importere hele den tilhørende assembly, og den returnerede MethodReference, gemmer jeg. Denne reference skal jeg bruge når jeg rent faktisk skal injecten.

Injecten har jeg besluttet mig for at lave i web applikationens Global.Application_Error metode. At finde denne metode er simpel

Mono.Cecil.TypeDefinition globalType = source.MainModule.Types.Where(x => x.Name == "Global").Single();
Mono.Cecil.MethodDefinition applicaionErrorMethod = globalType.Methods.Where(x => x.Name == "Application_Error").Single();

og dejlig letlæselig også takket være Linq.

Jeg har nu

  1. importeret den assembly som indeholder den metode som jeg vil kalde
  2. fundet metoden som injecten skal ske i

Nu kan jeg begynde at injecte:

Mono.Cecil.Cil.ILProcessor processor = applicaionErrorMethod.Body.GetILProcessor();
Mono.Cecil.Cil.Instruction call = processor.Create(Mono.Cecil.Cil.OpCodes.Call, reference);
Mono.Cecil.Cil.Instruction str = processor.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"h");

processor.InsertBefore(applicaionErrorMethod.Body.Instructions[0], str);
processor.InsertAfter(applicaionErrorMethod.Body.Instructions[0], call);

For at injecte fx kald i eksisterende kode, skal man først have fat i den kontekst som man vil injecte i. Vi vil gerne injecte noget kode Application_Error metoden, derfor skal vi have fat i kroppen af denne metode. Kroppen har Mono.Cecil pænt pakket ind til os, og de har endda gjort det nemt for os at injecte kode ind med ILProcessor’en. Jeg frygtede, før jeg gik igang, at man selv skulle til at flytte rundt på IL opkoder, og derved nemt ødelægge det eksisterende, men dette behøves man ikke med ILProcessoren. Den hjælper os nemlig. Både med at injecte, men også med at oprette MSIL instruktioner. Med Create metoden laver man instruktioner som skal injectes, og med InsertBefore, og InsertAfter, injecter man. Dette gør at koden er meget nemmere at læse. Man kan derfor fokusere mere på at skrive god og letlæseligt kode, og mindre på at forstå, og huske hvad MSIL kode der skal konstrueres.

Selve oprettelsen af det IL kode der skal injectes sker altså gennem ILProcessorens Create metode.

Mono.Cecil.Cil.Instruction call = processor.Create(Mono.Cecil.Cil.OpCodes.Call, reference);
Mono.Cecil.Cil.Instruction str = processor.Create(Mono.Cecil.Cil.OpCodes.Ldstr,"h");

Læg mærke til at jeg bruger WriteLine metodens reference, og ikke selve dens MethodDefinition. Jeg opretter to instruktioner. En som sørger for at kalde metoden, og en som sørger for at lægge metodens paramteren på stakken. For at injecte kaldet indsætter jeg disse to instruktioner i Application_Error’ens krop. Jeg vil gerne indsætte kaldet først i metoden

processor.InsertBefore(applicaionErrorMethod.Body.Instructions[0], str);
processor.InsertAfter(applicaionErrorMethod.Body.Instructions[0], call);

Her smider jeg strengen, “h”, på stakken og laver et fald til min WriteLine metode.

Rækkefølgen af indsættelsen er ikke hel ligegyldig. Hvis du ikke er inde i hvordan metodekald sker i de “mere primitive” sprog, så skal man, før man kalder metoden, smide alle metodens parametre på stakken, og derefter kalde metoden. Call koden sørger derefter for at tage de argumenter som den skal bruge fra stakken og oprette en ny stak, som den bruger til at udføre sin handling. Efter endt kald rykkes dens kaldstak ned, og en eventuel returværdi smides på kalderens stak, som kalderen kan vælge at bruge i dens videre forløb.

Tilsidst skal vi have gemt den modificerede assembly

source.MainModule.Write("WebApplicationModified.dll");

Der er nu gemt en modificeret assembly med Writer.WriteLine(“h”) kaldet i Application_Error metoden. Du kan se dette ved at inspicere den gemte dll med ILSpy. For at sikre sig at man ikke ødelagt og lavet en korrupt dll, kan man køre PEVerify på den. PEVerify tjekker om alt er som det skal være i den nyoprettede assembly. Har man injectet noget galt og usammenhængede MSIL kode, skal PEVerify nok fortælle dig det. Dette kunne fx være at man havde glemt at smide en parameter på stakken før man kalder en given metode.

udtalelserne i denne artikel er fra 2011, hvor jeg første gang udgav min artikel. Pr. dags dato (2015), er der sikkert kommet flere artikler rundt omkring på nettet, som forklarer hvordan man gør dette, dog finder jeg stadig denne artikel relevant, for de af jer (og ikke mindst mig selv), som gerne vil kigge lidt på AOP. Jeg har derfor valgt at genudgive artiklen, råt for usødet.

Et af de steder hvor app udvikling differentierer sig, hvis man kigger på udvikling af en app til windows desktop versus udvikling af en app til Windows Phone, er reordering af elementer i et listview via drag and drop. For man skal nemlig ikke gøre det samme på WP, som på desktop, for at aktivere denne form for reordering. Jeg vil tilskynde mig at sige, at det faktisk er lettere at aktivere på Windows Phone, da man kun skal sætte ReorderMode="Enabled", hvor imod man skal sætte AllowDrop og CanReorderItems til true på desktop.

For at fange en reordering, skal man sørge for at ens ListView er binded op på en ObservableCollection. På denne collection skal man lytte på CollectionChanged eventet.

Lad mig lave et eksempel. Hvis vi tager udgangspunkt i dette xaml

<Page>
    <Page.DataContext>
        <Runtime:ViewModel/>
    </Page.DataContext>
    <Grid>
        <ListView ItemsSource="{Binding Models}" ReorderMode="Enabled">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Text}"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Page>

Her har, som forskrevet, bundet mit ListView op til en property, Models, som er af typen ObservableCollection på min ViewModel, ved navn ViewModel. Koden til min ViewModel er som følger:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MVVM
{
    public class ViewModel : INotifyPropertyChanged
    {
        public System.Collections.ObjectModel.ObservableCollection<Model> Models { get; set; }
        private string _text;
        public string Text
        {
            get { return _text; }
            set
            {
                _text = value;
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs("Text"));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        public ViewModel()
        {
            Models = new System.Collections.ObjectModel.ObservableCollection<Model>();
            Models.CollectionChanged += Models_CollectionChanged;

            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
            Models.Add(new Model() { Text = "Text" });
        }

        void Models_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {

        }

    }
}

Her kan I se at Models er af typen ObservableCollection. Jeg har i min constructor, tilføjet en funktion, som skal køres når denne ObservableCollection ændres. Selve implementeringen af denne funktion har jeg undladt.

For de af jer der er nysgerrige, så ser min klasse Model således ud:

namespace MVVM
{
    public class Model : INotifyPropertyChanged
    {
        private string _text;
        public string Text { get { return _text; } set { 
            _text = value; 
            if(PropertyChanged !=null)
            PropertyChanged(this, new PropertyChangedEventArgs("Text")); 
        } }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Lige til. I skal se bort fra min dårlige kodestruktur og navngivning, da dette er underordnet, i dette eksempel. Jeg håber I kan se udover dette.

Det ser sker, når app'ens ListView bliver reorderet, er at den underliggende ObservableCollection fyrer eventet af, og kalder vores funktion, hvorefter vi kan reagere på det, og opdatere vores domæne kode.

Grunden til at jeg laver et indlæg om noget så trivielt er, at man til Windows Phone ikke skal kalde de samme properties, som man skal til desktop, for at opsætte hele reorderingen via drag and drop. Derfor kan det forvirre yderligere (som jeg gjorde i mit tilfælde), når man derefter skal finde ud af hvordan man skal lytte på selve reorderingen, for er dette det samme som på desktoppen, når nu det differentiere sig ved selve opsætningen? Det er det ikke, men det tog mig lidt tid at finde ud af, så derfor har jeg skrevet et indlæg om det. Det er desuden meget svært at google sig frem til løsningen med at man på Windows Phone skal aktivere reordering via drag and drop med ReorderMode="Enabled", og ikke AllowDrop og CanReorderItems til true som på desktop. Jeg fandt svaret i en kommentar på et svar til et spørgsmål på StackOverflow.

Mono.Cecil;

udgivet 1/25/2015 4:30:13 PM

Mono.Cecil er smart. Det er et bibliotek som kan bruges til manipulere managed dll filer efter de er kompileret. Det er et godt stykke værktøj hvis du fx står med en dll, som du ikke kan debugge, da du ikke source filen, eller måske ikke kan installere windbg på serveren. Her kan man hente dll’en ned, og fx smide trace information ind de steder som skal kigges efter i sømmene. Jeg har altid kigget lidt på code injection, da jeg synes det er sjovt. Man får styrket IL gymnastikken lidt, og får rørt lidt ved aspekt orienteret programmering (det er ihvertfald min indgangsvinkel til at lege med Mono.Cecil).

Jeg har fx lavet en lille WriterLibrary, som jeg gerne vil bruge i en anden dll, kaldet WebApplication.dll. Mit lille tankespil går ud på at WebApplication.dll er min, ikke source kode defineret, ikke windbg-able, applikation, som jeg gerne vil trace. WriterLibrary er min “store” debug og trace bibliotek som jeg gerne vil bruge til at kigge WebApplication efter i sømmene. Der er ikke meget dokumentation omkring Mono.Cecil på nettet, dog er det meget simpelt at komme igang med. For at komme igang vil jeg springe lige ud i det. Jeg har lavet et konsol program som ser sådan her ud

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CecilTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Mono.Cecil.AssemblyDefinition source =
                Mono.Cecil.AssemblyDefinition.ReadAssembly(@"C:sti\til\\WebApplication.dll");

            Mono.Cecil.AssemblyDefinition lib =
                Mono.Cecil.AssemblyDefinition.ReadAssembly(@"C:\sti\til\WriterLibrary.dll");

            source.MainModule.Import(lib.MainModule.Types[1]);
            source.MainModule.Write("WebApplicationModified.dll");
        }
    }
}

For at alt dette virker skal vi også have fat i Mono.Cecil, men dette er let. Man henter Mono.Cecil med NuGet. Åbn Package Manager Console og kør “Install-Package Mono.Cecil”. Kommandoen henter de dll’er der skal til for man kan gøre brug af Mono.Cecil. Min WriterLibrary er her blot en simpel class library som ser sådan her ud

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WriterLibrary
{
    public class Writer
    {
        public void WriteLine(string s)
        {
            Console.WriteLine("wrote:" + s);
        }
    }
}

Når man laver en Import på en hvilken som helst type fra WriterLibrary til WebApplication dll’en, så sørger Mono.Cecil også for at lave en reference til WriterLibrary dll’en. Du kan selv tjekke efter ved at inspicere WebApplication dll’en før konsol applikationen er kørt, og efter. Da man skal betale for Reflector, vil jeg råde alle til at bruge IlSpy. Google den, og hent programmet. Den kan ikke alt det som Reflector kan, men den er gratis, og man kan kigge på dll’er. Meget praktisk Smiley

Ved at inspicere WebApplication dll’en efter konsol applikationen er kørt, kan man se at referencen er tilføjet. Meget smart. Man kan nu bruge WriterLibrary til at lave trace på WebApplication. Jeg har endnu ikke lavet noget injection, men det er noget jeg vil skrive om efterfølgende, da jeg selv lige skal lære at bruge Mono.Cecil. Min vision med dette eksempel er at jeg vil prøve at injecte WriteLine funktionskaldet ind i WebApplication.dll’ens Global.Application_Error, da jeg ved at WebApplication gør brug af en Global.asax.

Håber at der er nogen der kan bruge mit lille eksempel til noget.