前言
你可以在這篇文章知道以下事情
- 讓WPF control寄生在一個Winform上面
- 只要編輯xaml就可以客制畫面,不用重新compile
- 如何在WPF裡使用Dependancy Injection ( 我看了N個範例發現不太容易找 )
- 如何在編輯時讓WPF的designer顯示跟runtime類似
- 有C# winform開發知識的人
- 沒空從頭開始看WPF
- 想試試看WPF,又有一個winform project可以玩
- 不知道怎麼對WPF做Dependancy Injeection
- 希望User可以自行修改畫面,但是又不用重新compile
- User懂一點xml, 但是不懂怎麼寫C#
先備知識
- MVVM-- Model-View-ViewModel: 有點類似MVC
- Model: 資料
- View: WPF designer顯示的畫面. 等同 winform UserControl.Designer.cs
- ViewModel: WPF的Control.Datacontext. 控制顯示的邏輯
- 在MVVM, code-behind 越少越好
- Code-Behind:
- 等同C# UserControl.cs
- Loose xaml:
- 沒有
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以上應該也可以適用- 打開 Example.sln.
- 點選 WPFHost project. 加上以下dll
- PresentationCore
- PresentationFramework
- System
- WindowsBase
- Form1.cs 示範winform怎麼 host WPF control.
- RSource 示範dependancy injection到 WPF usercontrol. 他會連上Device Register,在此不討論
- ctrlHost 負責WPF hosting,而且必須加進一個panel control裡面
- 根據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 ( 跟上面是同一個,只是怕你先跳過來看 )
- 打開WpfCustomControls project
- 右鍵點 UserControl1.xaml 選"View code",或展開他選UserControl1.xaml.cs.
- this.Content代表View的來源.
- this.DataContext 就是 ViewModel, 所以我要把 RSource丟進 CncViewModel.
- 注意 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,或是看下面的懶人版:
- {Binding PropertyOfViewModel} 讓你可以把資料直接綁到DataContext的properties上,注意只有properties, field不支援
- DataContext會從Parent繼承,除非有額外指定
( ref: What is this "DataContext" you speak of? )
我用ViewModel做了一些自製語法,並且有設定this.DataContext = new CncViewModel( m_Source );
- 因為要讓使用者自訂圖片與多國語系,我用特殊語法支援字串
- 使用 IIndexableGetter讓自訂語法 STR[InsertKeyHere]可以拿到多國語系字串,PIC[Filepath] 則會顯示特定位置的圖片.
- 注意
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
- Button.Command 示範如何使用CommandParameter
如果只有一行,使用CommandParameter="SomeSyntax".
- Example Button.CommandParameter below shows how to define an array of string in xaml.
- 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- 實作 MockDesignViewModel.
- xmlns:d的意思是使用 xaml blend namespace "d".
- mc:Ignorable="d" 表示runtime可忽略d
- d:DataContext 表示只在deisntime建立 MockDesignViewModel
- 顯示如果有問題,可以參考 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:
- Basic knowledge of WPF
- Understanding the change in mindset when switching from WinForms to WPF
- What is this "DataContext" you speak of?
- A Simple MVVM Example
- WPF Hosting
- Data binding
- ICommand
- DesignTime settings