书名:WPF专业编程指南
作者:李应保
出版社:电子工业出版社
出版时间:2010-01
ISBN:9787121100116
一、附加属性实例
- 这个例子中使用附加属性来实现编辑软件中常用的“重做/取消”功能。
WPF的Text Box及相关元素都内在支持“重做/取消”和“拷贝/复制”这样一些常用的编辑功能,但WPF的“重做/取消”功能只对具有当前输入焦点的TextBox有效。
在这里要做的是:“取消和重做”可以对多个TextBox同时进行。
思路:
- 为此,要把用户的操作用堆栈管理起来,当用户输入的时候,把用户的操作放入堆栈,当用户要取消一个输入的时候可把该输入放到另外一堆栈;
在用户要进行重做时,只要把用户的输入从另外一个堆栈中取出来就可以了。
由于堆栈的先入先出功能,可以很方便地管理用户“最近”的 操作。
设计
- 首先我们定义一个UndoOperation操作类:
public class UndoOperation
{
public TextBoxBase Sender;
public UndoAction Action;
public UndoOperation(TextBoxBase sender, UndoAction action)
{
this.Sender = sender;
this.Action = action;
}
}
这个类中有两个域,一个是Sender表示当前操作哪一个UI元素,其类型为TextBoxBase;
另一个是UndoAction。
WPF中TextBoxBase是TextBox及RichTextBox两个类的基类。
WPF中定义了一个UndoAction枚举类型,可取Create、Clear、none、Undo,Redo、Merge这样一些值。UndoService是主要完成“取消/重做”操作的类,其中含有两个管理“取消/重做”操作的堆栈:
Stack<UndoOperation> undoStack = new Stack<UndoOperation>();
Stack<UndoOperation> redoStack = new Stack<UndoOperation>();
- 如前所述,附加属性是相关属性的一种。和相关属性一样,需要定义一个public static类型为DependencyProperty的变量:
public static readonly DependencyProperty SharedUndoRedoScopeProperty =
DependencyProperty.RegisterAttached("SharedUndoRedoScope",
typeof(UndoService),
typeof(UndoService),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.Inherits, new
PropertyChangedCallback(OnUseGlobalUndoRedoScopeChanged)));
与相关属性不同,附加属性需要使用相关属性的RegisterAttached方法注册。
在注册的时候同样需要使用FrameworkPropertyMetadata类,来说明附加属性的一些特性。
这里 说明SharedUndoRedoScopeProperty是可以传递的(设定Inherits标志),并且设置了该属性值发生改变时要调用的处理函数。定义附加属性时,还要求定义两个读取附加属性的方法,其格式是固定的:
public static void SetSharedUndoRedoScope(DependencyObject depObj, bool
isSet)
{
// never place logic in here, because these methods are not called
when things are done in XAML
depObj.SetValue(SharedUndoRedoScopeProperty, isSet);
}
public static UndoService GetSharedUndoRedoScope(DependencyObject
depObj)
{
// never place logic in here, because these methods are not
called when things are done in XAML
return depObj.GetValue(SharedUndoRedoScopeProperty) as UndoService;
}
- 注意:这两个方法都有一个Dependency类型的参数,由此可知,附加属性只能附加到DependencyObject上,而且附加属性的值存储在该DependencyObject实例内!
理解这一点,是理解附加属性的关键,这就是附加的由来:
depObj.SetValue(SharedUndoRedoScopeProperty, isSet);
在SharedUndoRedoScopeProperty属性值发生变化时,WPF属性系统会调用在注册附加属性时设置的回调函数:
OnUseGlobalUndoRedoScopeChanged在这个回调函数中,设置或去除了相应的事项处理函数。WPF中的传递事件是一种新的事件类型,将在第7章深入讨论,这里的重点是附加属性。完整的UndoService类源程序如下:
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
namespace Yingbao.Chapter4
{
public class UndoService
{
Stack<UndoOperation> undoStack = new Stack<UndoOperation>();
Stack<UndoOperation> redoStack = new Stack<UndoOperation>();
#region Attached Properties
public static readonly DependencyProperty
SharedUndoRedoScopeProperty =
DependencyProperty.RegisterAttached(
"SharedUndoRedoScope", typeof(UndoService),
typeof(UndoService),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.Inherits, new
PropertyChangedCallback(
OnUseGlobalUndoRedoScopeChanged)));
public static void SetSharedUndoRedoScope(DependencyObject
depObj, bool isSet)
{
depObj.SetValue(SharedUndoRedoScopeProperty, isSet);
}
public static UndoService GetSharedUndoRedoScope(
DependencyObject depObj)
{
return depObj.GetValue(SharedUndoRedoScopeProperty) as
UndoService;
}
private static void OnUseGlobalUndoRedoScopeChanged(
DependencyObject depObj,
DependencyPropertyChangedEventArgs args)
{
if (depObj is TextBoxBase)
{
if (args.OldValue != null)
{
RemoveEventHandlers(depObj as TextBoxBase,
args.OldValue as UndoService);
}
if (args.NewValue != null)
{
AttachEventHandlers(depObj as TextBoxBase,
args.NewValue as UndoService);
}
}
}
private static void AttachEventHandlers(TextBoxBase textBox,
UndoService service)
{
if (textBox != null && service != null)
{
textBox.AddHandler(CommandManager.PreviewExecutedEvent,
new ExecutedRoutedEventHandler(
service.ExecutedHandler), true);
textBox.TextChanged += new TextChangedEventHandler(
service.TextChangedHandler);
}
}
private static void RemoveEventHandlers(TextBoxBase textBox,
UndoService service)
{
if (textBox != null && service != null)
{
textBox.RemoveHandler(CommandManager.PreviewExecutedEvent,
new ExecutedRoutedEventHandler(
service.ExecutedHandler));
textBox.TextChanged -= new
TextChangedEventHandler(service.TextChangedHandler);
}
}
#endregion
void TextChangedHandler(object sender, TextChangedEventArgs e)
{
this.AddUndoableAction(sender as TextBoxBase, e.UndoAction);
}
private void ExecutedHandler(object sender,
ExecutedRoutedEventArgs e)
{
if (e.Command == ApplicationCommands.Undo)
{
e.Handled = true;
Undo();
}
if (e.Command == ApplicationCommands.Redo)
{
e.Handled = true;
Redo();
}
}
private void AddUndoableAction(TextBoxBase sender,
UndoAction action)
{
if (action == UndoAction.Undo)
{
redoStack.Push(new UndoOperation(sender, action));
}
else
{
if (undoStack.Count > 0)
{
UndoOperation op = undoStack.Peek();
if ((op.Sender == sender) && (action ==
UndoAction.Merge))
{
// no-op
}
else
{
PushUndoOperation(sender, action);
}
}
else
{
PushUndoOperation(sender, action);
}
}
}
private void PushUndoOperation(TextBoxBase sender,
UndoAction action)
{
undoStack.Push(new UndoOperation(sender, action));
System.Diagnostics.Debug.WriteLine("PUSHED");
}
public void Undo()
{
if (undoStack.Count > 0)
{
UndoOperation op = undoStack.Pop();
op.Sender.Undo();
op.Sender.Focus();
}
}
public void Redo()
{
if (redoStack.Count > 0)
{
UndoOperation op = redoStack.Pop();
op.Sender.Redo();
op.Sender.Focus();
}
}
}
public class UndoOperation
{
public TextBoxBase Sender;
public UndoAction Action;
public UndoOperation(TextBoxBase sender, UndoAction action)
{
this.Sender = sender;
this.Action = action;
}
}
}
- 有了这个UndoService类,就可以对在窗口中的多个TextBox元素进行统一管理了。下面是使用UndoService的XAML的一个示例:
<Window x:Class="Yingbao.Chapter4.AttachedPropertyWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="UndoRedo" Height="600" Width="400"
xmlns:undo="clr-namespace:Yingbao.Chapter4">
<StackPanel>
<Border BorderBrush="Orange" BorderThickness="3" Margin="5"
CornerRadius="2">
<Grid Background="#feca00" >
<undo:UndoService.SharedUndoRedoScope>
<undo:UndoService />
</undo:UndoService.SharedUndoRedoScope>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="设定重做范围" FontSize="22" Foreground="#58290a"
Grid.ColumnSpan="2" />
<TextBlock Grid.Row="1" Grid.Column="0">姓名:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1" Margin="1" />
<TextBlock Grid.Row="2" Grid.Column="0">毕业院校:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1" Margin="1" />
<TextBlock Grid.Row="3" Grid.Column="0">工作简历:</TextBlock>
<RichTextBox Grid.Column="0" Grid.Row="4" Grid.ColumnSpan="2"
Margin="1" Height="70" />
</Grid>
</Border>
<Border BorderBrush="#58290a" BorderThickness="3" Margin="5"
CornerRadius="2">
<Border.Resources>
<!-- undo managers can also be created and used as resources -->
<undo:UndoService x:Key="undoService" />
</Border.Resources>
<Grid undo:UndoService.SharedUndoRedoScope="{StaticResource
undoService}" Background="LightGray " >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="设定重做范围" FontSize="22" Foreground="#58290a"
Grid.ColumnSpan="2" />
<TextBlock Grid.Row="1" Grid.Column="0">姓名:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1" Margin="1" />
<TextBlock Grid.Row="2" Grid.Column="0">毕业院校:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1" Margin="1" />
<TextBlock Grid.Row="3" Grid.Column="0">工作简历:</TextBlock>
<RichTextBox Grid.Column="0" Grid.Row="4" Grid.ColumnSpan="2"
Margin="1" Height="70" />
</Grid>
</Border>
<Border BorderBrush="DarkGreen" BorderThickness="3" Margin="5"
CornerRadius="2">
<Grid Background="LightGreen" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="未设定重做范围" FontSize="22"
Foreground="DarkGreen" Grid.ColumnSpan="2" />
<TextBlock Grid.Row="1" Grid.Column="0">姓名:</TextBlock>
<TextBox Grid.Row="1" Grid.Column="1" Margin="1" />
<TextBlock Grid.Row="2" Grid.Column="0">毕业院校:</TextBlock>
<TextBox Grid.Row="2" Grid.Column="1" Margin="1" />
<TextBlock Grid.Row="3" Grid.Column="0">工作简历:</TextBlock>
<RichTextBox Grid.Column="0" Grid.Row="4" Grid.ColumnSpan="2"
Margin="1" Height="70" />
</Grid>
</Border>
</StackPanel>
</Window>
-
图4-7是这段程序的运行结果,这个程序可以作为人事部门管理软件的基本界面。
需要指出的是:要对“取消/重做”功能进行测试,要用到标准的快捷键。“取消”的快捷键是“Ctrl+Z”;“重做”的快捷键是“Ctrl+Y”。这些快捷键在WPF中是内嵌的,不需要我们做什么工作。在上面的窗口中,前两个输入框都使用了SharedUndoRedoScope属性,而最后一个输入框没有使用SharedUndoRedoScope属性。
在前两个输入框的姓名、毕业院校、工作简历中输入信息,然后用快捷键“Ctrl+Z”和“Ctrl+Y”进行“取消/重做”操作,可以看到,这种操作对这三个TextBox都有效。而在最后一组中,这种操作只对具有当前输入焦点的TextBox有效。
图4-7 使用附加属性实现的“取消/重做”功能
