Monday, September 16, 2019

Quick Guide for Using WPF UserControl in Winform

Preface


In this article, you will know following things in a tiny example.
  1. Create a winform to host WPF control.
  2. Let user change layout by editing xaml without compilation, so you don't have to worry about version control of your user.
  3. Inject dependantcy in WPF from winform.
    ( It is easy, but I found it's hard to know when I have zero knowledge about WPF. )
  4. How to let user self design in WPF designer looks identical ( or similar ) as in runtime.
This article suites for who
  1. Having experience in winform C# development.
  2. Don't have time to start from scratch.
  3. Wants to give WPF a try in your winform project.
  4. Searching hard but don't know how to pass reference into WPF viewmodel.

Why would I want to try wpf:
  1. I want my users to change GUI layout without compilation.
  2. My users don't know how to write C# code, but they have some knowledge of xml.
Before running through the example, here are some terms you need to know.
  1. MVVM-- Model-View-ViewModel: Kind of similar to MVC.
    1. Model: Your data.
    2. View: What you see in WPF designer. Equivalent to winform UserControl.Designer.cs
    3. ViewModel: In a WPF control, it s your Control.Datacontext. It controls how view to display model. 
    4. In MVVM, code-behind is better to be as tiny as possible.
  2. Code-Behind:
    1. It is equivalent to C# UserControl.cs
  3. Loose xaml:
    1. Xaml without x:Class attribute, where x:Class is equivalent to winform designer partial class.

   First of all, you need a winform to host WPF control, and there is a good example in microsoft document

   Though this example is complete enough, it uses code-behinde to handle button click event.
This xaml of example is not loose xaml.. You can't load it in runtime property, unless doing something dirty (subscribing events) in code behind.
I will suggest to use ICommand interface to handle button click and it will be implemented in my example. I don't use any third-party library in this example, but you may want to take a look at MVVM Light  or Prism if you dig deeper in WPF.

How to host WPF Control

Example here. It is using dot net 4.0, but 4.5 or above will work as well.
  1. Click Example.sln.
  2. Look at WPFHost project. And following dlls are needed
    1. PresentationCore
    2. PresentationFramework
    3. System
    4. WindowsBase
  3. Form1.cs will show you how to host WPF control.
  4. RSource is dependency injected into WPF usercontrol. It is connected to device registers, but it's out of scope of this article.
  5. ctrlHost is hosting WPF control and MUST be added in a panel control.
  6. In microsoft document, this code of snippet is placed in Form_Load. I don't know what would went wrong if it's in ctor. Haven't try it yet.
  private void Form1_Load( object sender, EventArgs e )
  {
   RSource Source = new RSource();
   ctrlHost = new ElementHost();
   ctrlHost.Dock = DockStyle.Fill;
   panel1.Controls.Add( ctrlHost );
   wpfAddressCtrl = new WpfCustomControl.UserControl1( Source );
   wpfAddressCtrl.InitializeComponent();
   ctrlHost.Child = wpfAddressCtrl;
  } 


Make a WPF Control with MVVM

Example here ( same as above one, just in case you're jumping into here )
  1. Look at WpfCustomControls project
  2. Right click on UserControl1.xaml and choose "View code", or expand this control and click UserControl1.xaml.cs.
  3. this.Content is the View.
  4. this.DataContext is the ViewModel, and that's why I put my RSource reference in CncViewModel.
  5. Also in GetXamlWithoutxClassDirective, I remove x:Class attribute while loading. This method is pretty straight-forward but I can't find any substitute of it..
  void LoadXAMLMethod()
  {
   // Changing content need to be out of constructor
   try {
    System.Diagnostics.Debug.WriteLine( "LoadXaml" );
    string szActualPath = Path.GetFullPath( @".\..\..\..\WpfCustomControls\UserControl1.xaml" );
    string szXaml = GetXamlWithoutxClassDirective( szActualPath );
    UserControl rootObject = XamlReader.Parse( szXaml ) as UserControl;
    this.Content = rootObject;
    this.DataContext = new CncViewModel( m_Source );
   }
   catch( FileNotFoundException ex ) {
    MessageBox.Show( ex.Message.ToString() );
   }
  }

  private string GetXamlWithoutxClassDirective( string m_szFilepath )
  {
   string szFailedText = string.Empty;
   string szFullText = string.Empty;
   using( StreamReader sr = new StreamReader( m_szFilepath ) ) {
    szFullText = sr.ReadToEnd();
   }
   int nDirectiveStartIndex = szFullText.IndexOf( "x:Class" );
   if( nDirectiveStartIndex == -1 ) {
    return szFullText;
   }
   // finding x:Class="abcdeftg.gxy"
   int nStartQuote = szFullText.IndexOf( "\"", nDirectiveStartIndex + 1 );
   if( nStartQuote == -1 ) {
    return szFailedText;
   }
   int nEndQuote = szFullText.IndexOf( "\"", nStartQuote + 1 );
   if( nEndQuote == -1 ) {
    return szFailedText;
   }
   //  where "after" second " 
   int nDirectiveEndIndex = nEndQuote + 1;
   int nDirectiveLength = nDirectiveEndIndex - nDirectiveStartIndex;
   string szNotDirectiveText = szFullText.Remove( nDirectiveStartIndex, nDirectiveLength );
   return szNotDirectiveText;
  }

   You may find I'm loading UserControl1.xaml in runtime. Because I will use UserControl1.xaml as designer interface, so User may user xaml designer as GUI to design their own view.

How do I connect View to data?

    Look at Data Binding Overview would be better. Also I have simple summary here for you:
  1. Using {Binding PropertyOfViewModel} will let you bind properties of your DataContext directly. Only properties work, a field is not allowed.
  2. Every element of DataContext is inherit from parent until you assign a different one.
    (ref: What is this "DataContext" you speak of?)

   Here's my ViewModel and my syntax implementation.
Remember I set this.DataContext = new CncViewModel( m_Source );

  1. In my case, I need to let user put custom picture and string on xaml, so I need my own syntax to let WPF control get my string.
  2. I use IIndexableGetter to provide indexer of let STR[InsertKeyHere] to get my custom string syntax. Also PIC[Filepath] would also display the picture.
  3. You may see   ICommand MyCommand  . That is the implementation about ICommand and it will set you free from event. If 

 public class CncViewModel : INotifyPropertyChanged
 {
  public CncViewModel( RSource source )
  {
   m_R = source;
   m_Pic = new MockPicture();
   m_Str = new MockStringLookup();

   // Using a timer to update value
   tmr = new System.Timers.Timer();
   tmr.AutoReset = true;
   tmr.Interval = 300;
   tmr.Elapsed += M_Source_TimeTicked;
   tmr.Enabled = true;

  }
  Timer tmr;
  private void M_Source_TimeTicked( object sender, EventArgs e )
  {
   firePropChanged( "R" );
   firePropChanged( "STR" );
  }
  void firePropChanged( string szProp )
  {
   if( PropertyChanged == null ) {
    return;
   }
   PropertyChanged( this, new PropertyChangedEventArgs( szProp ) );
  }
  public IIndexableGetter<int> R
  {
   get
   {
    return m_R;
   }
  }
  public IIndexableGetter<string> STR
  {
   get
   {
    return m_Str;
   }
  }
  public IIndexableGetter<BitmapImage> PIC
  {
   get
   {
    return m_Pic;
   }
  }

  public ICommand MyCommand
  {
   get
   {
    return new RelayCommand( ParseSyntecCmd );
   }
  }

  public void ParseSyntecCmd( string[] arr )
  {
   Console.WriteLine( "Cmd fired" );
   if( arr == null ) {
    Console.WriteLine( "Null" );
    return;
   }
   foreach( var szStr in arr ) {
    // Implement string command here
    Console.WriteLine( "Prm: " + szStr );
   }
  }

  // private
  IIndexableGetter<BitmapImage> m_Pic;
  IIndexableGetter<int> m_R;
  IIndexableGetter<string> m_Str;
  public event PropertyChangedEventHandler PropertyChanged;
 }


 public class MockStringLookup : IIndexableGetter<string>
 {
  public string this[ string szIndex ]
  {
   get
   {
    // Implement your own custom string or multilanguage mechanism.
    string[] szSplit = szIndex.Split( ':' );
    if( szSplit.Length == 0 ) {
     return string.Empty;
    }
    return szSplit[ szSplit.Length - 1 ];
   }
  s}
 }sdfsd




    The xaml of a button named "CycleStart". ( It's not really called CycleStart in loose xaml, because loose xaml don't have x syntax. ).

  1. Button.Command show you how to bind multiply self defined syntax in Command
    1. If there is only one parameter, simply use  CommandParameter="SomeSyntax".
    2. Example Button.CommandParameter below shows how to define an array of string in xaml.
  2. Button.Style is a self explanatory demo. If R[6000] value is 1, background is pink. If R[6000] is 0, background is white.

        <Button
            Grid.Column="2"
            x:Name="CycleStart" 
            Margin="22,10,105,0" VerticalAlignment="Top" Height="49"
            Command="{Binding MyCommand}">
            <Button.CommandParameter>
                <x:Array Type="{x:Type s:String}">
                    <s:String>MySyntax_Inv R6000.0</s:String>
                    <s:String>MySyntax_R6001=2</s:String>
                </x:Array>
            </Button.CommandParameter>
            <Button.Style>
                <Style TargetType="{x:Type Button}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding R[6000]}"
                            Value="1">
                            <Setter Property="Background" Value="Pink" />
                        </DataTrigger>
                        <DataTrigger Binding="{Binding R[6000]}"
                            Value="0">
                            <Setter Property="Background" Value="White" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Button.Style>
            <Image x:Name="image" Height="43" Width="43" Source="{Binding PIC[Images\\play.png]}"/>
        </Button>

Simulate runtime display in WPF designer

Example here ( same as above one, just in case you're jumping into here )

   You might find some of display (eg: content strings) in designer is different from runtime. Rather than always run your program on runtime to see the change, all you need is a mocked ViewModel to display in designer. Also it will allow user to use intellisense in xaml which is a convenient side-effect of mocking ViewModel. Here are some notes:

  1. Implement your own MockDesignViewModel.
  2. xmlns:d xaml namespace aliasing as "d" in xaml. In this case, d is for blend to be shown in designtime.
  3. mc:Ignorable="d"  means ignore xmlns:d it on runtime.
  4. d:DataContext ...etc means it will create an instance of MockDesignViewModel on designtime.
  5. If you have troubles in getting design view right, How to debug in wpf design view would help a lot.


<--! Example of Usercontrol using a design instance >
<UserControl x:Class="WpfCustomControl.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:s="clr-namespace:System;assembly=mscorlib"
             xmlns:local="clr-namespace:WpfCustomControl"
             mc:Ignorable="d" 
             d:DesignHeight="800" d:DesignWidth="600"
             d:DataContext="{d:DesignInstance Type=local:MockDesignViewModel, IsDesignTimeCreatable=True}"
             >
</UserControl>


// Exmaple of a mock design view model.
 public class MockDesignViewModel
 {
  public MockDesignViewModel()
  {
   m_Pic = new MockPicture();
   m_R = new MockRegister();
   m_Str = new MockStringLookup();
  }

  public IIndexableGetter<BitmapImage> PIC
  {
   get
   {
    return m_Pic;
   }
  }

  public IIndexableGetter<int> R
  {
   get
   {
    return m_R;
   }
  }

  public IIndexableGetter<string> STR
  {
   get
   {
    return m_Str;
   }
  }

  IIndexableGetter<BitmapImage> m_Pic;
  IIndexableGetter<int> m_R;
  IIndexableGetter<string> m_Str;
 }

 public class MockStringLookup : IIndexableGetter<string>
 {
  public string this[ string szIndex ]
  {
   get
   {
    string[] szSplit = szIndex.Split( ':' );
    if( szSplit.Length == 0 ) {
     return string.Empty;
    }
    return szSplit[ szSplit.Length - 1 ];
   }
  }
 }

 public class MockRegister : IIndexableGetter<int>
 {
  public int this[ string szIndex ]
  {
   get
   {
    int nIndex = 0;
    bool bParse = Int32.TryParse( szIndex, out nIndex );
    return nIndex;
   }
  }
 }

 public class MockPicture : IIndexableGetter<BitmapImage>
 {
  public MockPicture()
  {
   // This is where solution is in design time
   string szWorkingDir = Environment.CurrentDirectory;

   // Get out of 3 layer folder
   m_szPictureDir = szWorkingDir;
  }

  public BitmapImage this[ string szIndex ]
  {
   get
   {
    string szCombinedpath = m_szPictureDir + "\\" + szIndex;
    string szActualPath = Path.GetFullPath( szCombinedpath );
    var uri = new Uri( szActualPath );
    BitmapImage bmp = new BitmapImage( uri );
    return bmp;
   }
  }
  // Private
  string m_szPictureDir;
 }


References:
  1. Basic knowledge of WPF
    1. Understanding the change in mindset when switching from WinForms to WPF
    2. What is this "DataContext" you speak of?
    3. A Simple MVVM Example
  2. WPF Hosting
    1. Walkthrough: Hosting a WPF Composite Control in Windows Forms.
  3. Data binding
    1. Data Binding Overview
  4. ICommand
    1. ICommand MVVM implementation
  5. DesignTime settings 
    1. How to debug in wpf design view

If there is any inaccurate description, that would be my fault. Please feel free to let me know what is wrong.

No comments:

Post a Comment