WPF – rotacja, transformacja i przechylanie

W ramach uczestnictwa w kursie WPF czytam ksiązkę “WPF 4 Unleashed” i przygotowuje jakieś przykłady sprawdzające co tam pan autor pisze w tej książce. Jeden z początkowych rozdziałów (tak, dopiero rozpoczynam kurs i książkę) poświęcony jest tematyce transformacji kontrolek, w związku z tym napisałem prosty przykład ukazyjący omawiane tranformacje. W zasadzie testuje tylko trzy z pięciu, ponieważ przesunięcie obiektów nie jest zbyt widowiskowe, a wykorzystanie macierzy nie jest na moją głowę. Zostają tylko obracanie, skalowanie i przechylanie.
Do implementacji wykorzystałem trzy wątki, a jak wszyscy wiemy .NET nie lubi, jak ktoś dobiera się do kontrolek z innego wątku niż głównego. Wykorzystany został tutaj obiekt Dispatcher z WPF, który ułatwia sprawę. Najbardziej denerwującą rzeczą okazał się brak możliwości skorzystania z wyrażeń lambda do implementacji ciała metody zajmującej się aktualizacją kontrolek. Trzeba było skorzystać z delegatów. Na stackoverflow widziałem przykład z rzutowaniem lambdy na Action. Po krótkiej i przegranej walce z przykładem, postanowiłem że zostanę przy delegatach.

Przykład jest na tyle prosty, że nie pokuszę się o wrzucanie go na jakiś serwer, zostanie pokazany światu tu i tylko tu.
Na początek UI (bez szału):

  1. <Window x:Class=”wpfTransformations.MainWindow”
  2.         xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
  3.         xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
  4.         Title=”MainWindow” Height=”466″ Width=”1000″>
  5.     <Grid>
  6.         <!– Rotating –>
  7.         <Button x:Name=”r_00_00″ Content=”0,0″ Height=”23″ HorizontalAlignment=”Left” Margin=”69,64,0,0″
  8.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,0″>
  9.             <Button.RenderTransform >
  10.                 <RotateTransform Angle=”33″ x:Name=”RotateTransform”/>
  11.             </Button.RenderTransform>
  12.         </Button>
  13.  
  14.         <Button x:Name=”r_10_00″ Content=”1,0″ Height=”23″ HorizontalAlignment=”Left” Margin=”255,64,0,0″
  15.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,0″>
  16.             <Button.RenderTransform>
  17.                 <RotateTransform Angle=”33″/>
  18.             </Button.RenderTransform>
  19.         </Button>
  20.  
  21.         <Button x:Name=”r_10_10″ Content=”1,1″ Height=”23″ HorizontalAlignment=”Left” Margin=”447,64,0,0″
  22.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,1″>
  23.             <Button.RenderTransform>
  24.                 <RotateTransform Angle=”33″/>
  25.             </Button.RenderTransform>
  26.         </Button>
  27.  
  28.         <Button x:Name=”r_00_10″ Content=”0,1″ Height=”23″ HorizontalAlignment=”Left” Margin=”631,64,0,0″
  29.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,1″>
  30.             <Button.RenderTransform>
  31.                 <RotateTransform Angle=”33″/>
  32.             </Button.RenderTransform>
  33.         </Button>
  34.  
  35.         <Button x:Name=”r_05_05″ Content=”0.5,0.5″ Height=”23″ HorizontalAlignment=”Left” Margin=”803,64,0,0″
  36.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0.5,0.5″>
  37.             <Button.RenderTransform>
  38.                 <RotateTransform Angle=”33″/>
  39.             </Button.RenderTransform>
  40.         </Button>
  41.         <!– Scaling –>
  42.           <Button x:Name=”s_00_00″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”69,179,0,0″
  43.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,0″>
  44.             <Button.RenderTransform>
  45.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  46.             </Button.RenderTransform>
  47.         </Button>
  48.         <Button x:Name=”s_00_10″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”255,179,0,0″
  49.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,1″>
  50.             <Button.RenderTransform>
  51.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  52.             </Button.RenderTransform>
  53.         </Button>
  54.         <Button x:Name=”s_10_10″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”447,179,0,0″
  55.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,1″>
  56.             <Button.RenderTransform>
  57.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  58.             </Button.RenderTransform>
  59.         </Button>
  60.         <Button x:Name=”s_10_00″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”631,179,0,0″
  61.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,0″>
  62.             <Button.RenderTransform>
  63.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  64.             </Button.RenderTransform>
  65.         </Button>
  66.         <Button x:Name=”s_05_05″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”803,179,0,0″
  67.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0.5,0.5″>
  68.             <Button.RenderTransform>
  69.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  70.             </Button.RenderTransform>
  71.         </Button>
  72.         <!– Skewing –>
  73.           <Button x:Name=”sk_00_00″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”69,315,0,0″
  74.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,0″>
  75.             <Button.RenderTransform>
  76.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  77.             </Button.RenderTransform>
  78.         </Button>
  79.         <Button x:Name=”sk_10_00″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”255,315,0,0″
  80.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,0″>
  81.             <Button.RenderTransform>
  82.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  83.             </Button.RenderTransform>
  84.         </Button>
  85.         <Button x:Name=”sk_10_10″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”447,315,0,0″
  86.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,1″>
  87.             <Button.RenderTransform>
  88.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  89.             </Button.RenderTransform>
  90.         </Button>
  91.         <Button x:Name=”sk_00_10″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”631,315,0,0″
  92.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,1″>
  93.             <Button.RenderTransform>
  94.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  95.             </Button.RenderTransform>
  96.         </Button>
  97.         <Button x:Name=”sk_05_05″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”803,315,0,0″
  98.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0.5,0.5″>
  99.             <Button.RenderTransform>
  100.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  101.             </Button.RenderTransform>
  102.         </Button>
  103.     </Grid>
  104. </Window>

Tak wygląda code-behind i trzy wątki odpowiedzialne, za aktualizację kontrolek (też bez szału):

  1. using System;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Media;
  5.  
  6. namespace wpfTransformations
  7. {
  8. using System.Threading;
  9.  
  10. /// <summary>
  11. /// Interaction logic for MainWindow.xaml
  12. /// </summary>
  13. public partial class MainWindow : Window
  14. {
  15.     // delegates to handle UI controls update from background threads
  16.     public delegate void SetAngleD(float f, Button b);
  17.     public delegate void SetScaleD(float sx, float sy, Button b);
  18.     public delegate void SetSkewD(float ax, float ay, Button b);
  19.  
  20.     // helper methods to update the controls    
  21.     public void SetAngle(float f, Button b)
  22.     {
  23.         RotateTransform r = new RotateTransform(f);
  24.         b.RenderTransform = r;
  25.     }
  26.  
  27.     public void SetScale(float sx, float sy, Button b)
  28.     {
  29.         ScaleTransform st = new ScaleTransform(sx, sy);
  30.         b.RenderTransform = st;
  31.     }
  32.  
  33.     public void SetSkew(float ax, float ay, Button b)
  34.     {
  35.         SkewTransform sk = new SkewTransform(ax, ay);
  36.         b.RenderTransform = sk;
  37.     }
  38.  
  39.     public MainWindow()
  40.     {
  41.         InitializeComponent();
  42.  
  43.         // This one will take care of rotation of the elements
  44.         Thread tRotatingThread = new Thread(
  45.             () =>
  46.             {
  47.                 float angle = 33.0f;
  48.                 while (true)
  49.                 {
  50.                     SetAngleD d = new SetAngleD(this.SetAngle);
  51.                     r_00_00.Dispatcher.Invoke(d, new object[] { angle, r_00_00 });
  52.                     r_00_10.Dispatcher.Invoke(d, new object[] { angle, r_00_10 });
  53.                     r_10_10.Dispatcher.Invoke(d, new object[] { angle, r_10_10 });
  54.                     r_10_00.Dispatcher.Invoke(d, new object[] { angle, r_10_00 });
  55.                     r_05_05.Dispatcher.Invoke(d, new object[] { angle, r_05_05 });
  56.  
  57.                     angle += 1.0f;
  58.                     if (angle > 360.0f)
  59.                     {
  60.                         angle = 0.0f;
  61.                     }
  62.  
  63.                     Thread.Sleep(10);
  64.                 }
  65.             });
  66.         tRotatingThread.IsBackground = true;
  67.  
  68.         // This one will play with scaling properties of controls
  69.         Thread tScalingThread = new Thread(
  70.             () =>
  71.             {
  72.                 float sxm = 0.019f;
  73.                 float sym = -0.011f;
  74.  
  75.                 float sx = 0.6f;
  76.                 float sy = -0.6f;
  77.  
  78.                 while (true)
  79.                 {
  80.                     SetScaleD d = new SetScaleD(this.SetScale);
  81.                     s_00_00.Dispatcher.Invoke(d, new object[] { sx, sy, s_00_00 });
  82.                     s_10_00.Dispatcher.Invoke(d, new object[] { sx, sy, s_10_00 });
  83.                     s_10_10.Dispatcher.Invoke(d, new object[] { sx, sy, s_10_10 });
  84.                     s_00_10.Dispatcher.Invoke(d, new object[] { sx, sy, s_00_10 });
  85.                     s_05_05.Dispatcher.Invoke(d, new object[] { sx, sy, s_05_05 });
  86.  
  87.                     if (Math.Abs(sx) > 2.1f)
  88.                     {
  89.                         sxm *= -1.0f;
  90.                     }
  91.  
  92.                     if (Math.Abs(sy) > 2.1f)
  93.                     {
  94.                         sym *= -1.0f;
  95.                     }
  96.  
  97.                     sx += sxm;
  98.                     sy += sym;
  99.                     Thread.Sleep(30);
  100.                 }
  101.             });
  102.  
  103.         tScalingThread.IsBackground = true;
  104.  
  105.  
  106.         // This one will skew them grrrrr
  107.         Thread tSkewingThread = new Thread(
  108.             () =>
  109.             {
  110.                 float ax = 33.0f;
  111.                 float ay = 3.0f;
  112.  
  113.                 while (true)
  114.                 {
  115.                     SetSkewD d = new SetSkewD(this.SetSkew);
  116.                     sk_00_00.Dispatcher.Invoke(d, new object[] { ax, ay, sk_00_00 });
  117.                     sk_10_00.Dispatcher.Invoke(d, new object[] { ax, ay, sk_10_00 });
  118.                     sk_10_10.Dispatcher.Invoke(d, new object[] { ax, ay, sk_10_10 });
  119.                     sk_00_10.Dispatcher.Invoke(d, new object[] { ax, ay, sk_00_10 });
  120.                     sk_05_05.Dispatcher.Invoke(d, new object[] { ax, ay, sk_05_05 });
  121.  
  122.                     ax += 1.0f;
  123.                     if (ax > 90.0f)
  124.                     {
  125.                         ax = 0.0f;
  126.                     }
  127.  
  128.                     ay += 1.0f;
  129.                     if (ay > 60.0f)
  130.                     {
  131.                         ay = 0.0f;
  132.                     }
  133.  
  134.                     Thread.Sleep(30);
  135.                 }
  136.             });
  137.  
  138.         tSkewingThread.IsBackground = true;
  139.  
  140.  
  141.         // Because everyone of them is set as background thread
  142.         // They should end nicely at the end of application
  143.         tRotatingThread.Start();
  144.         tScalingThread.Start();
  145.         tSkewingThread.Start();
  146.     }
  147.  
  148. }
  149. }

Na koniec aplikacji w akcji (odrobina szału jest):

Pierwszy rząd guzików jest obracany, kolejny skalowany, a ostatni przechylany. Chyba lepiej to wygląda gdy działa, po wklejeniu do msvc powinien śmigać bez zająknięcia.
To tyle. Do zapamiętania: Dispatcher nie przyjmie lamby i nie walcz z tym, od razu korzystaj z delegatów. Chyba, że wiesz o co chodzi z tym Action() i wiesz jak z tego skorzystać (wiesz? – napisz w komentarzu), albo masz inne rozwiązanie. Podobno w 4.5 można z każdego wątku aktualizować UI, jeszcze nie sprawdziłem.

ps. Rozpieszczam was tym zawijaniem wierszy.

Mono Cecil – przykład użycia

W poprzednim poście wspomniałem o mojej walce w Mono Cecil, dzisiaj chciałbym się podzielić moimi wrażeniami i doświadczeniem. Dodam tylko, że o Mono usłyszałem na spotkaniach wrocławskiej grupy .net, wykład prowadził Paweł Łukasik, slajdy z wykładu dostępne są na jego blogu: http://pawlos.blogspot.com. Jak zwykle zapraszam na spotkania i wykłady.

Źródła które pokaże, są tylko prostym przykładem, zamysłem tego co chciałem zrobić w docelowym rozwiązaniu. Powinny jednak wystarczyć by ukazać jak działa Mono Cecil.
Sztuczka miała polegać na dodaniu funkcjonalności do istniejącej już aplikacji; jestem leniwy i nie chce klikać myszą w guziczki góra, dół, prawo i lewo, chciałbym mieć możliwość nawigowania za pomocą strzałek na klawiaturze. Rozwiązania są dwa, można napisać taką implementacji od zera w IL i doklejeniu jej do docelowej aplikacji. Druga możliwość, to skopiowanie gotowej implementacji z innej aplikacji. Wszyscy jesteśmy leniwi, tak więc rozwiązanie drugie było bardziej kuszące.
Cały projekt jest dostępy na git hubie dostępy dla każdego.

Zabrałem się więc do roboty i oto do czego doszedłem:
Kod wykonujący brudną robotę:

  1. using System.Linq;
  2.  
  3. namespace mcWorker
  4. {
  5.     using Mono.Cecil;
  6.     using Mono.Cecil.Cil;
  7.  
  8.     class Program
  9.     {
  10.         static void Main(string[] args)
  11.         {
  12.             string sourceExe = @”contentSourceForm.exe”;
  13.             string targetExe = @”contentTargetForm.exe”;
  14.  
  15.             string srcMethod1 = “Form1_KeyDown”;
  16.             string srcMethod2 = “Form1_KeyUp”;
  17.  
  18.             // Source of code
  19.             AssemblyDefinition adSrc = AssemblyDefinition.ReadAssembly(sourceExe);
  20.             TypeDefinition typeSrc = adSrc.MainModule.Types
  21.                 .Where(t => t.Name == “SourceForm”).First();
  22.  
  23.  
  24.             // Place where code will be added
  25.             AssemblyDefinition adDst = AssemblyDefinition.ReadAssembly(targetExe);
  26.             TypeDefinition typeDst = adDst.MainModule.Types
  27.                 .Where(t => t.Name == “TargetForm”).First();
  28.  
  29.             // Copy both methods from src to dst
  30.             MethodDefinition m1 = CopyMethod(adSrc, typeSrc, srcMethod1, adDst, typeDst);
  31.             MethodDefinition m2 = CopyMethod(adSrc, typeSrc, srcMethod2, adDst, typeDst);
  32.  
  33.             // Now they should be marked as event handlers
  34.             AddEventHandlers(adDst, typeDst, “KeyDown”, m1);
  35.             AddEventHandlers(adDst, typeDst, “KeyUp”, m2);
  36.  
  37.             adDst.Write(@”newTarget.exe”);
  38.  
  39.         }
  40.  
  41.         private static void AddEventHandlers(AssemblyDefinition adDestination,
  42.                                             TypeDefinition typeDestination,
  43.                                             string aEventName,
  44.                                             MethodDefinition aEventHandler)
  45.         {
  46.             // For the simpliciyty of code the event handler will be connected
  47.             // to the events in default constructors
  48.             // Just before leaving it
  49.  
  50.             // I know that there is just one constructor, but this is only an example!
  51.             var ctor = typeDestination.Methods.Where(m => m.IsConstructor).First();
  52.  
  53.             // Find last return code
  54.             // We will put our code just before that opcode
  55.             var lastRet = ctor.Body.Instructions.Reverse()
  56.                 .Where(i => i.OpCode == OpCodes.Ret).First();
  57.  
  58.             // Now we need an IL generator
  59.             var ilg = ctor.Body.GetILProcessor();
  60.  
  61.             // and now the magic
  62.             ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldarg_0));
  63.             ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldarg_0));
  64.             ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldftn, aEventHandler));
  65.  
  66.             // I did check here also that there is only one construcor
  67.             ilg.InsertBefore(
  68.                 lastRet,
  69.                 Instruction.Create(
  70.                     OpCodes.Newobj,
  71.                     adDestination.MainModule
  72.                     .Import(typeof(System.Windows.Forms.KeyEventHandler)
  73.                     .GetConstructors().First())));
  74.  
  75.             ilg.InsertBefore(
  76.                 lastRet,
  77.                 Instruction.Create(
  78.                     OpCodes.Callvirt,
  79.                     adDestination.MainModule
  80.                     .Import(typeof(System.Windows.Forms.Control)
  81.                     .GetEvent(aEventName).GetAddMethod())));
  82.         }
  83.  
  84.         private static MethodDefinition CopyMethod(AssemblyDefinition adSource,
  85.                                                     TypeDefinition typeSource,
  86.                                                     string mthdName,
  87.                                                     AssemblyDefinition adDestination,
  88.                                                     TypeDefinition typeDestination)
  89.         {
  90.             // source
  91.             MethodDefinition srcMethod = typeSource.Methods
  92.                 .Where(m => m.Name == mthdName).First();
  93.  
  94.             // now create a new place holder for copy
  95.             MethodDefinition target = new MethodDefinition(srcMethod.Name,
  96.                                                         srcMethod.Attributes,
  97.                                                         adDestination.MainModule
  98.                                                         .Import(srcMethod.ReturnType));
  99.  
  100.             // Copy all method parameters
  101.             // I could use var, but I did this on purpose to show the type used.
  102.             foreach (ParameterDefinition pd in srcMethod.Parameters)
  103.             {
  104.                 target.Parameters.Add(
  105.                     new ParameterDefinition(pd.Name, pd.Attributes, adDestination.MainModule
  106.                         .Import(pd.ParameterType)));
  107.             }
  108.  
  109.             // Now copy all local variables that are defined withing method body
  110.             // I could use var, but I did this on purpose to show the type used.
  111.             foreach (VariableDefinition vd in srcMethod.Body.Variables)
  112.             {
  113.                 target.Body.Variables
  114.                     .Add(new VariableDefinition(adDestination.MainModule
  115.                         .Import(vd.VariableType)));
  116.             }
  117.  
  118.             // copy the state
  119.             target.Body.InitLocals = srcMethod.Body.InitLocals;
  120.  
  121.             /* copy all instructions from SRC to DST */
  122.             foreach (Instruction instruction in srcMethod.Body.Instructions)
  123.             {
  124.                 // Case when method call another method defined withing SRC type/assembly
  125.                 MethodReference mr = instruction.Operand as MethodReference;
  126.                 // Case when method load field from type/assembly
  127.                 FieldReference fr = instruction.Operand as FieldReference;
  128.                 TypeReference tr = instruction.Operand as TypeReference;
  129.                 if (mr != null)
  130.                 {
  131.                     if (mr.DeclaringType == typeSource)
  132.                     {
  133.                         // That would mean that here we have a
  134.                         // method call to method within source type
  135.                         // And this need to be redirected to source type
  136.                         // or handled in some other way
  137.                         // But in this example is not used
  138.                         // If you want some examples please contace me
  139.                     }
  140.                     else
  141.                     {
  142.                         target.Body.Instructions.Add(
  143.                             Instruction.Create(instruction.OpCode,
  144.                             adDestination.MainModule.Import(mr)));
  145.                     }
  146.                 }
  147.                 else
  148.                 {
  149.                     if (fr != null)
  150.                     {
  151.                         // So we migth found our selfs in position that we need
  152.                         // to redirect this load to some other field or remove it.
  153.                         // For now lets redirect for different field
  154.                         // Please try to remove the code between TRY ME
  155.                         // and check what peverify.exe will tell
  156.                         /*TRY ME*/
  157.                         if (fr.Name == “sourceStatus”)
  158.                         {
  159.                             target.Body.Instructions.Add(
  160.                                 Instruction.Create(
  161.                                     instruction.OpCode,
  162.                                     adDestination.MainModule.Import(typeDestination.Fields
  163.                                     .Where(f => f.Name == “targetStatus”).First())));
  164.                         }
  165.                         else/*TRY ME*/
  166.                         {
  167.                             target.Body.Instructions.Add(Instruction
  168.                                 .Create(instruction.OpCode, adDestination.MainModule.Import(fr)));
  169.                         }
  170.  
  171.                     }
  172.                     else if (tr != null)
  173.                     {
  174.                         target.Body.Instructions.Add(Instruction
  175.                             .Create(instruction.OpCode, adDestination.MainModule.Import(tr)));
  176.                     }
  177.                     else
  178.                     {
  179.                         target.Body.Instructions.Add(instruction);
  180.                     }
  181.                 } // else
  182.             } // foreach
  183.  
  184.             typeDestination.Methods.Add(target);
  185.             return target;
  186.         }
  187.     }
  188. }

Źródło z którego chce wziąć kod (zależy mi na obsłudze klawiszy).

  1. using System.Windows.Forms;
  2.  
  3. namespace SourceForm
  4. {
  5.     public partial class SourceForm : Form
  6.     {
  7.         public SourceForm()
  8.         {
  9.             InitializeComponent();
  10.         }
  11.  
  12.         private void Form1_KeyDown(object sender, KeyEventArgs e)
  13.         {
  14.             if (e.KeyCode == Keys.Escape)
  15.             {
  16.                 this.sourceStatus.Text = string.Format(“I did forget to mention that {0} ends the game.”, e.KeyCode.ToString());
  17.             }
  18.             else
  19.             {
  20.                 this.sourceStatus.Text = string.Format(“Key {0} (down).”, e.KeyCode.ToString());
  21.             }
  22.  
  23.         }
  24.  
  25.         private void Form1_KeyUp(object sender, KeyEventArgs e)
  26.         {
  27.             if (e.KeyCode == Keys.Escape)
  28.             {
  29.                 this.Close();
  30.             }
  31.             else
  32.             {
  33.                 this.sourceStatus.Text = string.Format(“Key {0} (up).”, e.KeyCode.ToString());
  34.             }
  35.         }
  36.  
  37.         private void Form1_MouseDown(object sender, MouseEventArgs e)
  38.         {
  39.             this.sourceStatus.Text = string.Format(“Mouse: {0} down”, e.Button.ToString());
  40.         }
  41.  
  42.         private void Form1_MouseUp(object sender, MouseEventArgs e)
  43.         {
  44.             this.sourceStatus.Text = string.Format(“Mouse: {0} up”, e.Button.ToString());
  45.         }
  46.     }
  47. }

Tak wygląda implementacja, którą chce rozszerzyć:

  1. using System.Windows.Forms;
  2.  
  3. namespace TargetForm
  4. {
  5.     public partial class TargetForm : Form
  6.     {
  7.         public TargetForm()
  8.         {
  9.             InitializeComponent();
  10.         }
  11.  
  12.         private void TargetForm_MouseDown(object sender, MouseEventArgs e)
  13.         {
  14.             this.targetStatus.Text = string.Format(“down: {0}”, e.Button.ToString());
  15.         }
  16.  
  17.         private void TargetForm_MouseUp(object sender, MouseEventArgs e)
  18.         {
  19.             this.targetStatus.Text = string.Format(“up: {0}”, e.Button.ToString());
  20.         }
  21.     }
  22. }

Komentarze w kodzie 🙂 Mam nadzieję że są w miarę zrozumiałe, jeżeli będzie coś niejasnego, zawsze służę pomocą.
Co ciekawe, warto spojrzeć (w źródła), że pola sourceStatus oraz targetStatus nie są tego samego typu, ale oba posiadają te same pola (Text) i oba dziedziczą po Control, dzięki temu pięknie zadziałał polimorfizm.

Oczywiste oczywistości:

  • Kod który podłącza event handlery do eventów nie wymyśliłem sam, z pomocą przyszedł reflektor. Zobaczyłem (czytaj skopiowałem) kod do obsługi myszy i wstawiłem analogiczny do obsługi klawiatury
  • Za pierwszym razem też wydawało mi się to strasznie zakręcone i okrutnie trudne, ale po trzecim podejściu do problemu, wszystko nabiera sensu. W sumie nawet fajnie się przegląda IL 😉 żarcik taki.
  • Nie wszystko się od razu udaje, przykład z TRY ME, za pierwszym razem (w trzecim podejściu) zapomniałem o tym i coś nie zadziałało. Na szczęście narzędzie peverify.exe potrafi o tym przypomnieć. Także możliwość debugowania dużo ułatwia, gdy można podejrzeć dokładnie wartości zmiennych i w razie potrzeby dla testów zmieniać jest w trakcie działania programu.
  • Google – szukajcie rozwiązań, jest spora szansa, że ktoś już miał problem podobny do waszego i został on rozwiązany. Jeżeli nie, to być może naprowadzi was na rozwiązanie waszego problemu. Nie warto odpuszczać, bo na pewno jakoś się da 🙂

Pokazany przykład jest prostym rozwiązaniem, a kod który przenosi funkcjonalność z aplikacji do aplikacji nie jest najbardziej rozbudowany i przemyślany. W przypadku, gdy pojawiają się dodatkowe wywołania metod w źródłowym assembly, zaczyna komplikować wszystko, trzeba sprawdzać nazwy metod, przypisywać referencję na docelowe assembly, pilnować typów, etc. Trzeba się bardziej nagimnastykować. Tutaj akurat nie chciałem się na tym skupiać.
Jeżeli coś nie działa sprawdzajcie reflektorem czy innym programem do podglądania kodu, czy nie zapomnieliście za importować gdzieś typu, lub czy wywołania się zgadzają. Czy wszystkie zmienne zostały za deklarowane w ciele (body) metody. Jeszcze raz przypominam o narzędziu peverify, które wskazuje co i gdzie jest nie tak.

Powodzenia i niech moc będzie z wami.
Jarek

//EDIT
Klient nasz pannn. Poprawiłem główny kod, teraz powinno być łatwiej go czytać. Ale nie chciało mi się tego robić dla form. Tam zresztą nie ma wiele ciekawego do oglądania.

Tips’n’Tricks – peverify

Od pewnego czasu walczę z mono cecil z mniejszym i większym powodzeniem, w trakcie walk znajduje na forach różne przydatne podpowiedzi, postanowiłem je zebrać w ramach krótkich wpisów. O mono też się pojawi wpis, ale jak już zrobię to na czym mi zależy i w dodatku będzie działać 🙂
Teraz słów kilka o peverify.exe.

PEVERIFY.EXE

Gdy chcemy się pobawić trochę systemem refleksji w c# czy hakować zasoby, czy w jakikolwiek inny sposób ingerować w skompilowany kod aplikacji pisanej w .net, a następnie mieć pewność że wszystko jest w porządku i nie zakłóciliśmy jej wewnętrznej harmonii warto sprawdzić ją przy pomocy peverify.
Sposób użycia jest banalnie prosty, jako parametr aplikacji podaje się nazwę programu do sprawdzenia i czeka na wynik.
Tak wygląda wynik takiej operacji:

Zakolorowałem część nazwy aplikacji, nigdy nie wiesz czy ktoś się nie wkurzy jak mu wytkniesz jego błędy 😉

Pierwszy widoczny błąd (stack underflow) to akurat moja wina, dodałem do kodu wywołania swojej funkcji, ale zapominałem wcześniej załadować pewien argument i proszę wszystko widać, nawet podaje linijkę gdzie wystąpił błąd. Reszta to rzeczy odziedziczone, co ciekawe widać aplikacja nie musi być idealna by można było z niej korzystać.
Aby uruchomić peverify wystarczy odpalić Visual Studio Command Prompt i działa, oczywista oczywistość można dodać sobie odpowiednie ścieżki do PATH i nie bawić w linie poleceń do msvc.

To tyle, krótko i na temat.

xUnit vs Event – jak go przetestować?

Rozpoczynam kolejny projekt, który ma przynieść mi chwałę, sławę i pieniądze. Piszę go przy wykorzystaniu TDD (jak zwykle angielska wiki ma więcej do powiedzenia) Wszystko szło ładnie do momentu gdy nie natrafiłem na test w którym chciałem sprawdzić czy klasa którą testuję wywoła event. No bo jak sprawdzić teraz coś co wykona się później?
Na szczęście wujek google zna programistów, którzy znają odpowiedź na takie i inne pytania, w związku z czym szybko znalazłem odpowiedź na swój problem.
Zamieszczę ja tutaj ze stosownymi komentarzami od siebie, plus kilka własnych uwag, które może ktoś uzna za wartościowe. Jest taki oto sobie kod:

  1. // arrange
  2. var cut = new AsyncImageServiceDownloader();
  3. var isp = new ImagesServiceProvider();
  4. const string ServiceAddress = “http://www.digart.pl/”;
  5. const string PageSuffix = “/przegladaj/nowe.html?p=”;
  6. isp.ServiceAddress = ServiceAddress;
  7. isp.PageAddressSuffix = PageSuffix;
  8. cut.AddProvider(isp);
  9. int downloaded = 0;
  10. int canceled = 0;
  11. int suspended = 0;
  12. int resumed = 0;
  13. const uint Requested = 30;
  14. ManualResetEvent synvEvent = new ManualResetEvent(false);
  15.  
  16. // act
  17. cut.AddProvider(isp);
  18. cut.PageDownloaded += (s, e) => { downloaded++; };
  19. cut.PageDownloadCanceled += (s, e) => { canceled++; };
  20. cut.PageDownloadSuspended += (s, e) => { suspended++; };
  21. cut.PageDownloadResumed += (s, e) => { resumed++; };
  22.  
  23. cut.DownloadPageAsync(0, 0, Requested);
  24. for (uint i = 0; i < Requested; ++i)
  25. {
  26.     cut.SuspendDownload(0, i);
  27. }
  28.  
  29. synvEvent.WaitOne(1000);
  30. for (uint i = 0; i < suspended; ++i)
  31. {
  32.     cut.ResumeDownload(0, i);
  33. }
  34.             
  35. synvEvent.WaitOne(15 * 1000);
  36. // assert
  37. Assert.That(downloaded, Is.EqualTo(Requested));
  38.  

Widzimy podział na trzy AAA (nie znalazłem polskiego).
Na początku przygotowuje środowisko, które będę chciał sprawdzić; cut (class under test) to właściwy obiekt, który chce sprawdzić, isp służy do poprawnego zainicjalizowania obiektu cut. Ważny obiekt ManualResetEvent jest deklarowany i tworzony w linii 14 posłuży mi do synchronizacji i czekania na eventy. Linie od 18 do 21 to podpięcie się pod eventy, którymi klasa może rzucić. Choć w tym przypadku interesują mnie tylko dwa: suspended oraz downloaded. Przy użyciu wyrażeń lambda podpinam się pod eventy, a jedyną rzecz jaką robię to zliczam ich wywołania. W tym teście nie interesuje mnie zawartość argumentów.
Skoro wszystko mam już gotowe do działania to ogień! Linia 23 wywołuje asynchroniczne zaciąganie 30 elementów (requested=30). Chwilę potem do akcja wkracza pętla z niecierpiącym zwłoki poleceniem SuspendDownload następnie daję chwilę testowanemu obiektowi na wykonanie kodu odpowiedzialnego za wywołanie odpowiednich eventów. Wszystko dzięki wywołaniu ManualResetEvent.WaitOne(1000) – specjalnie podany jest timeout, ponieważ nigdzie nie będzie zmieniony jego stan. Czas się kończy, testy wykonują się dalej. Kolejne rozkazy; ResumeDownload tyle razy ile razy zdążyłem go zawiesić. I kolejny odpoczynek z timeout 15 sekund, co pozwala spokojnie zakończyć się wszystkim operacjom ściągnięcia.
Na sam koniec sprawdzam czy rzeczywiście udało się ściągnąć tyle ile początkowo żądałem. Voila! To tyle, proste prawda?

Moje dwie uwagi:
– Pamiętaj że w obsłudze eventa możesz samodzielnie zmienić stan ManualEventReset, jeśli z tego skorzystasz nie zapomnij jednak dodać timeout do WaitOne w przeciwnym wypadku, w razie niepowodzenia testów ManualResetEvent zawiesi działanie testów.
– Zastanów się czego dokładnie oczekujesz od twojego cut. Jeżeli cut wywoła PageDownloaded trzy razy to od razu będziesz szczęśliwy? A może chcesz sprawdzić czy cut wywoła PageDownloaded tylko trzy razy, a żadnych innych eventów ani razu.

Jakieś uwagi? Inne sposoby? Literówka?
Wiesz jak mnie znaleźć.
JS

Palcem po mapie – CultureInfo w natarciu

Przygotowania do 70-536 idą sobie idą. Miałem ostatni mały sparing z CultureInfo, zatem trzeba się podzielić wiedzą, którą wyniosłem z tej krótkiej walki.
CultureInfo zawiera wszystkie informacje, których będziemy potrzebować, żeby aplikacja była poprawnie językowa (oczywiście trzeba znać jeszcze język obcy). Zaczynając od tego po której stronie kartki zaczynamy pisać, poprzez znak oddzielający ułamek od części całkowitej, powoli sunąc przez nazwę kalendarza, który jest używany i wreszcie kończąc na znaku, którym oznacza się nieskończoność. To tylko część z informacji, które posiada ta klasa.
Z CultureInfo można skorzystać poprzez podanie go jako parametr IFormatProvider (ToString, string.Format, StringBuilder.AppendFormat) lub poprzez ustawienie go w aktualnym wątku jako aktualną kulturę metodą Thread.CurrentThread.CurrentUICulture = myNewCultureInfo;
Kawałek kodu, dzięki któremu można się bliżej poznać z kulturami z całego świata. Wszystko dzięki wbudowanemu wsparciu dla multi-kuli w windows.

  1. private void Form1_Load(object sender, EventArgs e)
  2. {
  3.     foreach (var ci in CultureInfo.GetCultures(CultureTypes.InstalledWin32Cultures))
  4.     {
  5.         this._avaiableCultures.Items.Add(ci.Name + “, “+ci.EnglishName+”( “+ci.NativeName+” )”);
  6.     }
  7.     this._avaiableCultures.SelectedIndex = 0;
  8. }
  9.  
  10. private void _avaiableCultures_SelectedIndexChanged(object sender, EventArgs e)
  11. {
  12.     string culture = this._avaiableCultures.SelectedItem.ToString().Substring(0,
  13.                      this._avaiableCultures.
  14.                      SelectedItem.ToString().
  15.                      IndexOf(“,”));
  16.  
  17.     StringBuilder sb = new StringBuilder();
  18.     CultureInfo ci = new CultureInfo(culture);
  19.     sb.AppendLine(“CALENDAR:”);
  20.     sb.AppendLine(ci.DateTimeFormat.NativeCalendarName);
  21.     sb.AppendLine(“DAYS:”);
  22.     foreach (var dayName in ci.DateTimeFormat.DayNames)
  23.     {
  24.         sb.AppendLine(dayName);
  25.     }
  26.     sb.AppendLine(“MONTHS:”);
  27.     foreach (var monthName in ci.DateTimeFormat.MonthNames)
  28.     {
  29.         sb.AppendLine(monthName);
  30.     }
  31.  
  32.     sb.AppendFormat(“NUMBERS:nFractions go after: {0}n”, ci.NumberFormat.CurrencyDecimalSeparator);
  33.     sb.AppendFormat(“Currency symbol: {0}n”, ci.NumberFormat.CurrencySymbol);
  34.     sb.AppendFormat(“Infinite symbol: {0}n”, ci.NumberFormat.PositiveInfinitySymbol);
  35.     this._localizedInfo.Text = sb.ToString();
  36.    
  37.     //CultureInfo myNewCultureInfo = new CultureInfo(culture);
  38.     //Thread.CurrentThread.CurrentUICulture = myNewCultureInfo;
  39.  
  40.     this._moneyValue.Text = string.Format(ci, “{0:C}”, 12345.67);
  41.     this._currentTime.Text = DateTime.Now.ToString(ci);
  42.  
  43.     if( ci.TextInfo.IsRightToLeft )
  44.     {
  45.         this._currentTime.RightToLeft = RightToLeft.Yes;
  46.         this._avaiableCultures.RightToLeft = RightToLeft.Yes;
  47.         this._currentTime.RightToLeft = RightToLeft.Yes;
  48.         this._localizedInfo.RightToLeft = RightToLeft.Yes;
  49.         this._moneyValue.RightToLeft= RightToLeft.Yes;
  50.     }
  51.     else
  52.     {
  53.         this._currentTime.RightToLeft = RightToLeft.No;
  54.         this._avaiableCultures.RightToLeft = RightToLeft.No;
  55.         this._currentTime.RightToLeft = RightToLeft.No;
  56.         this._localizedInfo.RightToLeft = RightToLeft.No;
  57.         this._moneyValue.RightToLeft = RightToLeft.No;
  58.     }
  59. }

Komentarz:
@01 – Na załadowanie formy, odczytuje wszystkie wspierane kultury. Następnie wrzucam informacje o nich do combobox.
@12  – Po wybrania innej kultury, sprawdzam jej skróconą nazwę.
@18 – Tworzę nową oraz wypisuje na jej podstawie nazwę kalendarza (@20), wszystki dni(@21), wszystkie miesiące(@26). Na koniec podstawowe jednostki płatnicze(@32). Przed wyjściem sprawdzam, czy w danej kulturze pisze się od lewej czy od prawej (@43) i aktualizuje kontrolki.

A tak wygląda aplikacja w działaniu: