前言
你可以在這篇文章知道以下事情
- 讓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:Classattribute的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
No comments:
Post a Comment