2009年4月30日 星期四

[WPF] 相依屬性(Dependency Property) 進階探討

實作 Dependency Property(相依屬性) 的規則與流程

1、首先必須宣告 Dependency Property

Dependency Property 欄位都是 publicstatic,並且名稱以「Property」作為結尾。

例如以下宣告:
public static readonly DependencyProperty IsDefaultProperty;

2、在 static 建構子中註冊 Dependency Property

由於 Dependency Property 必須呼叫 static 方法 DependencyProperty.Register() 來建立

而在註冊時,必須在 Register() method 中指定以下內容:
  • Dependency Property 的名稱

  • Dependency Property 的型態

  • 包含 Dependency Property 的 Class 型態

  • Metadata (型態為 FrameworkPropertyMetadata,而 Callback Function 宣告於此處)

  • 用來檢查 Property 的值是否合法的 function


3、設定 Getter 與 Setter

Dependency Property 宣告了就是用來設定或取值之用的,當然要設定 Getter 與 Setter 囉!

但比較特別的是,Dependency Property 值的設定或取得,是透過 System.Windows.DependencyObject 中的 SetValue()GetValue() 來處理的。

而比較需要注意的是,若是有任何的程式邏輯或是檢查要加入,請記得加到 Callback Function 中,別加在 Getter 與 Setter 中,因為那只是用來取值或設定值之用。
(所有 WPF 內建的屬性封裝都遵守此規則,因此別亂寫囉!)



使用 Dependency Property 的優點 & 所提供的功能

1、節省記憶體

想像一下,Button 類別有將近 100 個欄位值,每個 Label 也將近有 90 個欄位值,若是畫面中有一堆 Button 與 Label 所產生的物件,記憶體的耗用量豈不是很可觀?

沒錯,這就是 Dependency Property 所要解決的問題,透過 static 的方式先產生 Dependency Property,在將 Button 與 Label 物件的屬性「依附上去」。

當每個 Button 或是 Label 都依附到同一個 Dependency Property,如此一來,記憶體的用量就會大大地減少囉!


2、變更告知 (Change Notification)

WPF 可以根據 Dependency Property 的 Metadata 自動觸法許多動作,例如:重新整理元素、更新畫面配置、重新整理資料繫結...等等。

範例:以下程式實作滑鼠移到按鈕上後,更改文字顏色(Button 的 IsMouseOver 屬性被更改,自動觸發更改文字顏色)

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="About WPF Unleashed" SizeToContent="WidthAndHeight" Background="OrangeRed" FontSize="30" FontStyle="Italic">





Chapter 1
Chapter 2








You have seccessfully registered this product.





3、屬性值繼承 (Property Value Inheritance)

元素樹中,父節點的控制項屬性會向下套用到子系的控制項屬性。

但也是有些例外的情況,原因可能會有兩種:
  1. 在 DependencyProperty.Register() 裡,透過 FrameworkPropertyMetadata.Options.Inherits 設定為不繼承父系控制項的屬性值。

  2. 子系控制項覆寫了繼承來的屬性值。


4、支援多重提供者

WPF 中有許多屬性提供者可用不同的方式設定 Dependency Property,而屬性值的決定過程會經過五個步驟:
  1. 判斷基底值

    這個部分會由多種屬性提供者來提供值,優先順序為:

    Local Value -> Style Trigger -> Template Trigger -> Style Setter -> Theme Style Trigger -> Theme Style Setters -> Property Value Inheritance -> Default Value

  2. 驗算

    此部份進行屬性值的驗算,為運算式。

  3. 套用動畫

    若有一個以上的動畫正在執行,它們可以改變目前的屬性值,甚至完全置換掉。

  4. 轉換

    若有註冊 CoerceValueCallback 委派,則屬性值會被傳遞到此 function 進行轉換,以取得更新後的值。
    (例如 ProgressBar 就是利用 callback function 來保持屬性值不會超過最大或最小值)

  5. 驗證

    最後還會進行屬性值的驗證,決定該值是否合法。(若有設定 ValidateValueCallback 委派)


5、附加屬性 (Attached Property)


附加屬性(attached property)是種特殊的 Dependency Property,可以附加到任何物件上,以下直接用範例說明:

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="About WPF Unleashed" Background="OrangeRed">





Chapter 1
Chapter 2








You have seccessfully registered this product




從程式碼可以看出,透過附加屬性的使用,StackPanel 中的控制項就不用一一加上 FontSize & FontStyle 兩個屬性了。


參考資料

Anita 的.NET 世界: 什麼是Dependency Property

design studio: Dependency Property

WPF Tutorial | Introduction to DependencyProperties

2009年4月21日 星期二

[.NET] SqlConnectionStringBuilder & SQL Express

想必應該有人使用 SqlConnectionStringBuilder 來產生 SQL 字串吧!

一般連到 SQL Server 時,在 DataSource 的部分都是指定 server 的 domain name 或是 IP address

但若是連到 SQL Express 呢??

以下以 localhost 為例,應該要輸入:
DataSource = "(local)\SQLEXPRESS";
如此一來就會產生正確的資料庫連線字串了~

2009年4月16日 星期四

[Windows] 使用 .reg 檔案新增 & 刪除登錄檔機碼

今天剛好遇到這個需求(使用 .reg 檔案新增 & 刪除登錄檔機碼),所以在網路上找到了蠻不錯的參考資料

如何使用登錄檔(.reg)進行新增、修改或刪除登錄機碼和值

利用登錄檔來刪除機碼 - 教學文章 - 卡拉娛樂網 論壇|娛樂|影音|分享|下載


新增比較容易,而刪除的話可以開啟登錄檔編輯程式(開始 -> 執行 -> regedit),將要刪除的機碼匯出,並進行編輯,在每個機碼前面加上「-」即可。

2009年4月7日 星期二

[Oracle] 如何使用流水號

比起 MS SQL Server 或是 MySQL,Oracle 使用流水號可以說是相當麻煩阿.......

怎麼說呢?

因為 int 欄位無法直接透過遞增的方式產生流水號,而是必須使用 sequence 搭配 trigger 來進行

因此整個過程必須分為三個步驟:
  1. 建立 table

  2. 建立 sequence,用來取得新的遞增值

  3. 建立 trigger,在新的資料存入 table 時,將新的遞增值存入 table 中

以下直接用範例來說明:


-- 建立 sequence 作流水號之用
CREATE SEQUENCE seq_user_group
INCREMENT BY 1
START WITH 1
ORDER;


-- 組別
CREATE TABLE user_group (
group_id NUMBER NOT NULL, -- 群組 ID
group_name NVARCHAR2(100), -- 組別名稱
remark NVARCHAR2(200) -- 備註
parent_group_id NUMBER, --父組別 ID

CONSTRAINT pk_user_group PRIMARY KEY(group_id)
);
COMMENT ON TABLE user_group is '組別';
COMMENT ON COLUMN user_group.group_id is '群組 ID';
COMMENT ON COLUMN user_group.group_name is '組別名稱';
COMMENT ON COLUMN user_group.remark is '備註';


-- 建立 trigger 以產生遞增欄位 (for user_group)
CREATE TRIGGER tri_auto_increment_user_group
BEFORE INSERT ON user_group
FOR EACH ROW
BEGIN
SELECT seq_user_group.NEXTVAL INTO :NEW.group_id FROM DUAL;
END;

[WPF] Routed 輸入事件

Routed Event

假設 element tree 如下:

Window
|
Grid
|
---------------------------
| | |
Button TextBlock ScrollViewer
| |
TextBlock StackPanel
|
TextBlock


而 Routed Event Handler 大概長以下這樣:
void functionName(object sender, RoutedEventArgs args)
{ ....(事件處理)... }
其中參數的部份有幾個重點:
  • sender:此物件將 event 送到 application 中

  • args.Source:真正導致 event 發生的 element


若是在 element tree 中左下角的 TextBlock 按一下滑鼠右鍵,分別會產生出以下的 event:
PreviewMouseDown -> MouseDown
其中 sender 的部份,觸發 event 順序如下:
  • PreviewMouseDown:Window -> Grid -> Button -> TextBlock

  • MouseDown:TextBlock -> Button -> Grid -> Window

因此從上面可以知道,PreviewMouseDown 是屬於 tunneling eventMouseDown 屬於 bubbling event

而透過 Event Routing 的特性,若是要讓較上層的 element 先處理 mouse down 的動作,可以撰寫在 PreviewMouseDown 中。



Mouse Event

MouseDown 與 MouseUp 不一定會同時出現

例如:在視窗中按下按鈕,會發生 MouseDown event,但如果持續按著不放,再離開視窗的範圍後再放開滑鼠,不會出現 MouseUp event。

因此,若要解決此問題,可以透過在 UIElementContentElement 中所定義的 CaptureMouse 方法來解決。

當呼叫了 CaptureMouse 方法後,即使滑鼠離開視窗,視窗也會持續的接收 MouseMove 與 MouseUp 事件。

最後當 MouseUp 事件處理完成後,可以呼叫 ReleaseMouseCapture 或是設定 Mouse.Capture 為 null 來釋放所捕捉的滑鼠。



Keyboard Event

在 WPF 中,keyboard event 都會下放到 TextBox、RichTextBox、ScrollViewer 這一類的控制項中作處理。

需要注意的是,只有當 element 或是控制項具有鍵盤焦點時,才會是 keyboard event 的來源。

此外,keyboard event 亦屬於 routed event,因此具有焦點的 element 的所有上層 element,都可以參與 keyboard event,且其 IsKeyboardFocusWithin 屬性都會是 true。

當 element 獲得 or 失去焦點時,會觸發 GotKeyboardFocusLostKeyboardfocus 事件,也都是 routed event;此外,由於兩個事件屬於 tunneling event,因此在 element tree 中上方的 element 可以先察覺到焦點的改變。

而 Keyboard 的輸入包含了三種 event,發生順序如下:
  1. KeyDown (參數型態為 KeyEventArgs)

  2. TextInput (參數型態為 TextCompositionEventArgs)

  3. KeyUp (參數型態為 KeyEventArgs)

但比較需要注意的是,若按下的是「Shift」、方向鍵、或是其他功能鍵,並不會產生 TextInput 事件。舉例來說,或先按下 Shift 再按 A,一共會觸發兩次 KeyDown 事件、一次 TextInput 事件、兩次 KeyUp 事件。

2009年4月1日 星期三

[WPF] 相依屬性(Dependency Property) 初探

在傳統的 Window Form 中的設計中,每個控制項都各自擁有自己的 property,而這些 property 大多都是相同的(例如:FontSize),因此不僅浪費記憶體來儲存這些 property 的值,有時在程式中還需要撰寫多餘程式來設定這些 property 的值。

因此,在 WPF 中提出了Dependency Property,用來解決各控制項中重複的 property 過多因此造成浪費記憶體的現象。

以下用範例程式來說明:(節錄自 Application = Code + Markup 一書)

SpaceButton.cs

using System;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace CH08.SetSpaceProperty
{
public class SpaceButton : Button
{
//傳統 .NET 做法:私有欄位搭配公開 property
string _txt;

//透過 get & set 設定 property 的處理方式
public string Text
{
set
{
_txt = value;
this.Content = SpaceOutText(_txt);
}
get { return _txt; }
}

//Dependecy Property 的宣告
public static readonly DependencyProperty SpaceProperty;
public int Space
{
//不同與以往 property 的設定方式
//Dependency Property 必須透過 DependencyObject 中的 SetValue() 與 GetValue() 進行 value 的處理
//*** SetValue() 與 GetValue() 皆為 static method ***
set { SetValue(SpaceProperty, value); }
get { return (int)GetValue(SpaceProperty); }
}

//處理 Dependency Property 必須使用 static constructor
static SpaceButton()
{
//設定 metadata (可以想像成是 Dependency Property 的相關特性)
FrameworkPropertyMetadata meta = new FrameworkPropertyMetadata();
meta.DefaultValue = 1;
meta.AffectsMeasure = true;
meta.Inherits = true;
meta.PropertyChangedCallback += OnSpacePropertyChanged;

//註冊 DependecyProperty
SpaceProperty = DependencyProperty.Register("Space", typeof(int), typeof(SpaceButton), meta, ValidateSpaceValue);
}

//value 檢驗時呼叫方法
static bool ValidateSpaceValue(object obj)
{
int i = (int)obj;
return i >= 0;
}


//當 property 改變時,呼叫此方法
static void OnSpacePropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
SpaceButton btn = obj as SpaceButton;
btn.Content = btn.SpaceOutText(btn._txt);
}

//在 Button Content 中每個字元間塞指定數量(Space)的空白
string SpaceOutText(string str)
{
if (str == null)
return null;

StringBuilder build = new StringBuilder();
foreach (char ch in str)
build.Append(ch + new string(' ', Space));

return build.ToString();
}
}
}



SpaceWindow.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace CH08.SetSpaceProperty
{
public class SpaceWindow : Window
{
//設定 Dependency Property
public static readonly DependencyProperty SpaceProperty;

public int Space
{
//同樣的,這邊也要透過 DependencyObject 中的 SetValue() 與 GetValue() 進行 value 的處理
set { SetValue(SpaceProperty, value); }
get { return (int)GetValue(SpaceProperty); }
}

//處理 Dependency Property 必須使用 static constructor
static SpaceWindow()
{
//設定 metadata
FrameworkPropertyMetadata meta = new FrameworkPropertyMetadata();
meta.Inherits = true;

//由於 SpaceProperty 在 SpaceButton 中已經宣告過
//因此這邊直接將 SpaceProperty 指到 SpaceButton 中的 SpaceProperty
//只要透過 AddOwner() 將自己也設定為該 Dependency Property 的擁有者即可
SpaceProperty = SpaceButton.SpaceProperty.AddOwner(typeof(SpaceWindow));

//唯一需要注意的是 metadata 並不會一同被採用
//因此需要另外的宣告與設定,並 override
SpaceProperty.OverrideMetadata(typeof(SpaceWindow), meta);
}
}
}



SetSpaceProperty.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace CH08.SetSpaceProperty
{
public class SetSpaceProperty : SpaceWindow
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new SetSpaceProperty());
}

public SetSpaceProperty()
{
this.Title = "Set Space Property";
this.SizeToContent = SizeToContent.WidthAndHeight;
this.ResizeMode = ResizeMode.CanMinimize;

int[] iSpaces = { 0, 1, 2 };
Grid grid = new Grid();
this.Content = grid;

//設定 Grid 的 RowDefinition
for (int i = 0; i < 2; i++)
{
RowDefinition row = new RowDefinition();
row.Height = GridLength.Auto;
grid.RowDefinitions.Add(row);
}

//設定 Grid 的 ColumnDefinition
for (int i = 0; i < iSpaces.Length; i++)
{
ColumnDefinition col = new ColumnDefinition();
col.Width = GridLength.Auto;
grid.ColumnDefinitions.Add(col);
}

for (int i = 0; i < iSpaces.Length; i++)
{
SpaceButton btn = new SpaceButton();
btn.Text = "Set window Space to " + iSpaces[i];
btn.Tag = iSpaces[i];
btn.HorizontalAlignment = HorizontalAlignment.Center;
btn.VerticalAlignment = VerticalAlignment.Center;
btn.Click += WindowPropertyOnClick;
grid.Children.Add(btn);
Grid.SetRow(btn, 0);
Grid.SetColumn(btn, i);

btn = new SpaceButton();
btn.Text = "Set button Space to " + iSpaces[i];
btn.Tag = iSpaces[i];
btn.HorizontalAlignment = HorizontalAlignment.Center;
btn.VerticalAlignment = VerticalAlignment.Center;
btn.Click += ButtonPropertyOnClick;
grid.Children.Add(btn);
Grid.SetRow(btn, 1);
Grid.SetColumn(btn, i);
}
}

//更改 Window 中的 Dependency Property
void WindowPropertyOnClick(object sender, RoutedEventArgs args)
{
SpaceButton btn = args.Source as SpaceButton;

//修改 Window 的 Space Dependency Property
//而此修改會影響所有在 SpaceWindow 中且擁有 SpaceProperty 屬性的控制項
//唯獨已經設定 Space 值的 SpaceButton 控制項例外
Space = (int)btn.Tag;
}

//更改 Button 中的 Dependency Property
void ButtonPropertyOnClick(object sender, RoutedEventArgs args)
{
SpaceButton btn = args.Source as SpaceButton;

//僅單獨修改 SpaceButton 中的 Space 值
//不會影響到 SpaceWindow 中其他擁有 SpaceProperty 的控制項
btn.Space = (int)btn.Tag;
}
}
}



參考資料

[WPF] Layout

Panel

ContentControl 中衍生出來的 class 都有 Content 屬性,幾乎可以被設定為任何物件,但問題在於「Content 只能設定一個物件」。

然而一個視窗中怎麼可能只放一個物件呢? 因此在 WPF 中使用了 Panel 作為解決此問題的方式,而 Panel 的階層與衍生類別如下:

UIElement

每種不同的 panel 有其自訂的 layout model,例如:

StackPanel 用水平或垂直 stack 的方式安排子元素

WrapPanel 類似 StackPanel,但可以將自動將元素擺到下一個 columm 或是 row 的位置

DockPanel 則會根據 parent container 的內部邊界來進行自動擺設

Grid 則是把所有子元素以格子狀的方式擺設

UniformGrid 則是所有的行都等寬,列都等高

Canvas 則是最貼近原本的做法,每個元素都有自己的座標位置


Dock

重點就在於 dock 的位置了,分別是 Top、Bottom、Left、Right 四種,如果將控制項的 HorizontalAlignment 設定為 Center,整個版面會變得很奇怪,因此在撰寫上要注意一下。

element 的定位必須使用 DockPanel.SetDock() 來處理。


Grid

蠻龜毛的一個 layout panel,必須要先定義 RowDefinition 以及 ColumnDefinition 並加入到 Grid 中,才能將控制項加入到 Grid 中。

而控制項加入時還要透過 Grid.SetRow() 以及 Grid.SetColumn() 來設定控制項在 Grid 中的位置。

element 的定位必須使用 Grid.SetRow()Grid.SetColumn()Grid.SetRowSpan()Grid.SetColumnSpan() 來處理。


Canvas


element 的定位必須使用 Canvas.SetLeft()Canvas.SetTop()Canvas.SetRight()Canvas.SetBottom() 等方法來處理。


事件處理

若將 event 處理指定給特定的控制項,例如:

Button.Click += ButtonOnClick;

void ButtonOnClick(object sender, RoutedEventArgs args)
{
MessageBox.Show("Hello");
}


當然,也可以把 event 透過 this.AddHandler 註冊給 Window 物件(包含 Button),但是就要在 function 中透過 args.Source 來判斷控制項的型態了,設定方式如下:

this.AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonOnClick));


既然可以將 event 註冊給 Window 物件,當然也可以註冊給 Panel 物件囉!