Tuesday, September 17, 2019

如何透過Winform使用Wpf UserControl

English version

前言


你可以在這篇文章知道以下事情
  1. 讓WPF control寄生在一個Winform上面
  2. 只要編輯xaml就可以客制畫面,不用重新compile
  3. 如何在WPF裡使用Dependancy Injection ( 我看了N個範例發現不太容易找 )
  4. 如何在編輯時讓WPF的designer顯示跟runtime類似
這邊文章適合
  1. 有C# winform開發知識的人
  2. 沒空從頭開始看WPF
  3. 想試試看WPF,又有一個winform project可以玩
  4. 不知道怎麼對WPF做Dependancy Injeection
為甚麼我要用WPF
  1. 希望User可以自行修改畫面,但是又不用重新compile
  2. User懂一點xml, 但是不懂怎麼寫C#
先備知識
  1. MVVM-- Model-View-ViewModel: 有點類似MVC
    1. Model: 資料
    2. View: WPF designer顯示的畫面. 等同 winform UserControl.Designer.cs
    3. ViewModel: WPF的Control.Datacontext. 控制顯示的邏輯
    4. 在MVVM, code-behind 越少越好
  2. Code-Behind:
    1. 等同C# UserControl.cs
  3. Loose xaml:
    1. 沒有 x:Class attribute的xaml, x:Class 等同 winform designer partial class.

   第一步: 建立一個Winform來Host WPF control, 可參考 microsoft document

    這個範例很好,但是他使用code-behind去處理click event,這樣會讓xaml變成非loose xaml,讓他不能被動態讀取,或是讀取出來還要用code-behind做一堆事情,才譨處理,我會建議用ICommand這個介面去處理click,在後面的範例也會提到,如果對WPF比較熟了,建議看看MVVM Light,  Prism 幫你處理繁瑣的事情

如何在Winform放入WPF control

Example here. 範例使用.Net 4.0, 4.5以上應該也可以適用
  1. 打開 Example.sln.
  2. 點選 WPFHost project. 加上以下dll
    1. PresentationCore
    2. PresentationFramework
    3. System
    4. WindowsBase
  3. Form1.cs 示範winform怎麼 host WPF control.
  4. RSource 示範dependancy injection到 WPF usercontrol. 他會連上Device Register,在此不討論
  5. ctrlHost 負責WPF hosting,而且必須加進一個panel control裡面
  6. 根據microsoft document,必須把這段程式放在Form_Load內,我還沒測試過放在別的地方會怎麼樣
  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;
  } 


建立一個符合MVVM的WPF control

Example here ( 跟上面是同一個,只是怕你先跳過來看 )
  1. 打開WpfCustomControls project
  2. 右鍵點 UserControl1.xaml 選"View code",或展開他選UserControl1.xaml.cs.
  3. this.Content代表View的來源.
  4. this.DataContext 就是 ViewModel, 所以我要把 RSource丟進 CncViewModel.
  5. 注意 GetXamlWithoutxClassDirective,讀取loose xaml的時候要記得把 x:Class attribute 移除,我暫時沒找到更好的方法
  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;
  }

   可以發現我直接在runtime 讀UserControl1.xaml in runtime. 這樣User就能直接把xaml desiner當作設計用GUI.

如何連結View跟Model (Data)?

    可以參考Data Binding Overview,或是看下面的懶人版:
  1. {Binding PropertyOfViewModel} 讓你可以把資料直接綁到DataContext的properties上,注意只有properties, field不支援
  2. DataContext會從Parent繼承,除非有額外指定
    ( ref: What is this "DataContext" you speak of? )

   我用ViewModel做了一些自製語法,並且有設定this.DataContext = new CncViewModel( m_Source );

  1. 因為要讓使用者自訂圖片與多國語系,我用特殊語法支援字串
  2. 使用 IIndexableGetter讓自訂語法 STR[InsertKeyHere]可以拿到多國語系字串,PIC[Filepath] 則會顯示特定位置的圖片.
  3. 注意 ICommand MyCommand 他讓xaml變成loose xaml, 不再需要event handler

 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






  1. Button.Command 示範如何使用CommandParameter
    1. 如果只有一行,使用CommandParameter="SomeSyntax".
    2. Example Button.CommandParameter below shows how to define an array of string in xaml.
  2. Button.Style應該滿好懂得,R[6000]=1背景是粉紅色  R[6000]=0變成白色

        <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>

WPF Designer模擬runtime顯示

Example here

   使用了自訂語法之後,Designer就沒辦法正常顯示資料,因為要透過runtime的資料才能正常顯示尺寸(有些control設定時,會根據字型大小改變,一個方式是每次改好就執行程式,另一個方式則是建立MockViewModel讓它顯示於Designer上,另一個副作用是xaml intellisense也會有用,下面是注意事項

  1. 實作 MockDesignViewModel.
  2. xmlns:d的意思是使用 xaml blend namespace  "d". 
  3. mc:Ignorable="d" 表示runtime可忽略d
  4. d:DataContext 表示只在deisntime建立 MockDesignViewModel
  5. 顯示如果有問題,可以參考 How to debug in wpf design view 


<--! 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

No comments:

Post a Comment