第20章 Property与Attribute
你在上一章遇到的XamlReader.Load方法可能会是一个方便的编程工具。假设你有一个TextBox,而且你为TextChanged事件设置了处理函数。当你在此TextBox内键入XAML,此TextChanged事件处理函数可以试着将XAML传递给XamlReader.Load方法,并显示出结果对象。你需要将XamlReader.Load调用放在try区块中,因为在输入XAML的过程中,大多数的时候,XAML都是无效的(invalid),但是这样的编程工具可以立即响应你的XAML实验。这会是一个很棒的XAML学习辅助工具,也是很有趣的工具。
这就是XAML Cruncher程序的前提。它绝对不是第一个这一类的程序,也不会是最后一个。XAML Cruncher是建立在Notepad Clone之上的。你将会看到, XAML Cruncher用一个Grid取代Notepad Clone客户区的TextBox。此Grid让TextBox占用一个格子,一个Frame控件占用另一个,两者之间是一个GridSplitter。当你键入TextBox的XAML被XamlReader.Load成功地转成一个对象,此对象会成为Frame的Content。
此XamlCruncher工程包含NotepadClone工程中的每一个文件,只有一个不包含在内: NotepadCloneAssemblyInfo.cs。此文件用下面的文件取代:
XamlCruncherAssemblyInfo.cs
//---------------------------------------------------------
// XamlCruncherAssemblyInfo.cs (c) 2006 by Charles Petzold
//---------------------------------------------------------
using System.Reflection;
[assembly: AssemblyTitle("XAML Cruncher")]
[assembly: AssemblyProduct("XamlCruncher")]
[assembly: AssemblyDescription("Programming Tool Using XamlReader.Load")]
[assembly: AssemblyCompany("www.charlespetzold.com")]
[assembly: AssemblyCopyright("\x00A9 2006 by Charles Petzold")]
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("1.0.0.0")]
你应该记得,NotepadCloneSettings类包含数个被保存为用户偏好的项目。XamlCruncherSettings类继承自NotepadCloneSettings,只加入3个项目。第一个是Orientation,负责TextBox和Frame的方向。XAML Cruncher有一个菜单项,让你可以将TextBox和Frame的其中一个放在另一个上面,或者让两者同时并列出现。另外,XAML将正常的TextBox对Tab按键的处理改掉,改成插入空格(space)。第二个用户偏好,是Tab按键所插入空格的数字。
第三个用户偏好是一个简单的XAML字符串,当你第一次执行此程序,或者当你选取File菜单的New菜单项,该字符串就会出现在TextBox中。一个菜单项让你可以将目前TextBox的内容,设定为此启动文件项目。
XamlCruncherSettings.cs
//-----------------------------------------------------
// XamlCruncherSettings.cs (c) 2006 by Charles Petzold
//-----------------------------------------------------
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Petzold.XamlCruncher
{
public class XamlCruncherSettings : Petzold.NotepadClone.NotepadCloneSettings
{
// Default settings of user preferences.
public Dock Orientation = Dock.Left;
public int TabSpaces = 4;
public string StartupDocument =
"<Button xmlns=\"http://schemas.microsoft.com/winfx" +
"/2006/xaml/presentation\"\r\n" +
" xmlns:x=\"http://schemas.microsoft.com/winfx" +
"/2006/xaml\">\r\n" +
" Hello, XAML!\r\n" +
"</Button>\r\n";
// Constructor to initialize default settings in NotepadCloneSettings.
public XamlCruncherSettings()
{
FontFamily = "Lucida Console";
FontSize = 10 / 0.75;
}
}
}
除此之外,XamlCruncherSettings构造函数将默认的字体改成10 point的Lucida Console。当然,当你执行XAML Cruncher时,你可以改变成你想要的字体。
下面是XamlCruncher类,继承自NotepadClone类。此类负责建立一个Grid作为Window新的内容,也建立Xaml顶层菜单项和其子菜单中的6个项目。
XamlCruncher.cs
//---------------------------------------------
// XamlCruncher.cs (c) 2006 by Charles Petzold
//---------------------------------------------
using System;
using System.IO; // for StringReader
using System.Text; // for StringBuilder
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives; // for StatusBarItem
using System.Windows.Input;
using System.Windows.Markup; // for XamlReader.Load
using System.Windows.Media;
using System.Windows.Threading; // for DispatcherUnhandledExceptionEventArgs
using System.Xml; // for XmlTextReader
namespace Petzold.XamlCruncher
{
class XamlCruncher : Petzold.NotepadClone.NotepadClone
{
Frame frameParent; // To display object created by XAML.
Window win; // Window created from XAML.
StatusBarItem statusParse; // Displays parsing error or OK.
int tabspaces = 4; // When Tab key pressed.
// Loaded settings.
XamlCruncherSettings settingsXaml;
// Menu maintenance.
XamlOrientationMenuItem itemOrientation;
bool isSuspendParsing = false;
[STAThread]
public new static void Main()
{
Application app = new Application();
app.ShutdownMode = ShutdownMode.OnMainWindowClose;
app.Run(new XamlCruncher());
}
// Public property for menu item to suspend parsing.
public bool IsSuspendParsing
{
set { isSuspendParsing = value; }
get { return isSuspendParsing; }
}
// Constructor.
public XamlCruncher()
{
// New filter for File Open and Save dialog boxes.
strFilter = "XAML Files(*.xaml)|*.xaml|All Files(*.*)|*.*";
// Find the DockPanel and remove the TextBox from it.
DockPanel dock = txtbox.Parent as DockPanel;
dock.Children.Remove(txtbox);
// Create a Grid with three rows and columns, all 0 pixels.
Grid grid = new Grid();
dock.Children.Add(grid);
for (int i = 0; i < 3; i++)
{
RowDefinition rowdef = new RowDefinition();
rowdef.Height = new GridLength(0);
grid.RowDefinitions.Add(rowdef);
ColumnDefinition coldef = new ColumnDefinition();
coldef.Width = new GridLength(0);
grid.ColumnDefinitions.Add(coldef);
}
// Initialize the first row and column to 100*.
grid.RowDefinitions[0].Height =
new GridLength(100, GridUnitType.Star);
grid.ColumnDefinitions[0].Width =
new GridLength(100, GridUnitType.Star);
// Add two GridSplitter controls to the Grid.
GridSplitter split = new GridSplitter();
split.HorizontalAlignment = HorizontalAlignment.Stretch;
split.VerticalAlignment = VerticalAlignment.Center;
split.Height = 6;
grid.Children.Add(split);
Grid.SetRow(split, 1);
Grid.SetColumn(split, 0);
Grid.SetColumnSpan(split, 3);
split = new GridSplitter();
split.HorizontalAlignment = HorizontalAlignment.Center;
split.VerticalAlignment = VerticalAlignment.Stretch;
split.Height = 6;
grid.Children.Add(split);
Grid.SetRow(split, 0);
Grid.SetColumn(split, 1);
Grid.SetRowSpan(split, 3);
// Create a Frame for displaying XAML object.
frameParent = new Frame();
frameParent.NavigationUIVisibility = NavigationUIVisibility.Hidden;
grid.Children.Add(frameParent);
// Put the TextBox in the Grid.
txtbox.TextChanged += TextBoxOnTextChanged;
grid.Children.Add(txtbox);
// Case the loaded settings to XamlCruncherSettings.
settingsXaml = (XamlCruncherSettings)settings;
// Insert "Xaml" item on top-level menu.
MenuItem itemXaml = new MenuItem();
itemXaml.Header = "_Xaml";
menu.Items.Insert(menu.Items.Count - 1, itemXaml);
// Create XamlOrientationMenuItem & add to menu.
itemOrientation =
new XamlOrientationMenuItem(grid, txtbox, frameParent);
itemOrientation.Orientation = settingsXaml.Orientation;
itemXaml.Items.Add(itemOrientation);
// Menu item to set tab spaces.
MenuItem itemTabs = new MenuItem();
itemTabs.Header = "_Tab Spaces...";
itemTabs.Click += TabSpacesOnClick;
itemXaml.Items.Add(itemTabs);
// Menu item to suppress parsing.
MenuItem itemNoParse = new MenuItem();
itemNoParse.Header = "_Suspend Parsing";
itemNoParse.IsCheckable = true;
itemNoParse.SetBinding(MenuItem.IsCheckedProperty,
"IsSuspendParsing");
itemNoParse.DataContext = this;
itemXaml.Items.Add(itemNoParse);
// Command to reparse.
InputGestureCollection collGest = new InputGestureCollection();
collGest.Add(new KeyGesture(Key.F6));
RoutedUICommand commReparse =
new RoutedUICommand("_Reparse", "Reparse",
GetType(), collGest);
// Menu item to reparse.
MenuItem itemReparse = new MenuItem();
itemReparse.Command = commReparse;
itemXaml.Items.Add(itemReparse);
// Command binding to reparse.
CommandBindings.Add(new CommandBinding(commReparse,
ReparseOnExecuted));
// Command to show window.
InputGestureCollection collGest = new InputGestureCollection();
collGest.Add(new KeyGesture(Key.F7));
RoutedUICommand commShowWin =
new RoutedUICommand("Show _Window", "ShowWindow",
GetType(), collGest);
// Menu item to show window.
MenuItem itemShowWin = new MenuItem();
itemShowWin.Command = commShowWin;
itemXaml.Items.Add(itemShowWin);
// Command binding to show window.
CommandBindings.Add(new CommandBinding(commShowWin,
ShowWindowOnExecuted, ShowWindowCanExecute));
// Menu item to save as new startup document.
MenuItem itemTemplate = new MenuItem();
itemTemplate.Header = "Save as Startup _Document";
itemTemplate.Click += NewStartupOnClick;
itemXaml.Items.Add(itemTemplate);
// Insert Help on Help menu.
MenuItem itemXamlHelp = new MenuItem();
itemXamlHelp.Header = "_Help...";
itemXamlHelp.Click += HelpOnClick;
MenuItem itemHelp = (MenuItem)menu.Items[menu.Items.Count - 1];
itemHelp.Items.Insert(0, itemXamlHelp);
// New StatusBar item.
statusParse = new StatusBarItem();
status.Items.Insert(0, statusParse);
status.Visibility = Visibility.Visible;
// Install handler for unhandled exception.
// Comment out this code when experimenting with new features
// or changes to the program!
Dispatcher.UnhandledException += DispatcherOnUnhandledException;
}
// Override of NewOnExecute handler puts StartupDocument in TextBox.
protected override void NewOnExecute(object sender,
ExecutedRoutedEventArgs args)
{
base.NewOnExecute(sender, args);
string str = ((XamlCruncherSettings)settings).StartupDocument;
// Make sure the next Replace doesn't add too much.
str = str.Replace("\r\n", "\n");
// Replace line feeds with carriage return/line feeds.
str = str.Replace("\n", "\r\n");
txtbox.Text = str; isFileDirty = false;
}
// Override of LoadSettings loads XamlCruncherSettings.
protected override object LoadSettings()
{
return XamlCruncherSettings.Load(typeof(XamlCruncherSettings),
strAppData);
}
// Override of OnClosed saves Orientation from menu item.
protected override void OnClosed(EventArgs args)
{
settingsXaml.Orientation = itemOrientation.Orientation;
base.OnClosed(args);
}
// Override of SaveSettings saves XamlCruncherSettings object.
protected override void SaveSettings()
{
((XamlCruncherSettings)settings).Save(strAppData);
}
// Handler for Tab Spaces menu item.
void TabSpacesOnClick(object sender, RoutedEventArgs args)
{
XamlTabSpacesDialog dlg = new XamlTabSpacesDialog();
dlg.Owner = this;
dlg.TabSpaces = settingsXaml.TabSpaces;
if ((bool)dlg.ShowDialog().GetValueOrDefault())
{
settingsXaml.TabSpaces = dlg.TabSpaces;
}
}
// Handler for Reparse menu item.
void ReparseOnExecuted(object sender, ExecutedRoutedEventArgs args)
{
Parse();
}
// Handlers for Show Window menu item.
void ShowWindowCanExecute(object sender, CanExecuteRoutedEventArgs args)
{
args.CanExecute = (win != null);
}
void ShowWindowOnExecuted(object sender, ExecutedRoutedEventArgs args)
{
if (win != null)
win.Show();
}
// Handler for Save as New Startup Document menu item.
void NewStartupOnClick(object sender, RoutedEventArgs args)
{
((XamlCruncherSettings)settings).StartupDocument = txtbox.Text;
}
// Help menu item.
void HelpOnClick(object sender, RoutedEventArgs args)
{
Uri uri = new Uri("pack://application:,,,/XamlCruncherHelp.xaml");
Stream stream = Application.GetResourceStream(uri).Stream;
Window win = new Window();
win.Title = "XAML Cruncher Help";
win.Content = XamlReader.Load(stream);
win.Show();
}
// OnPreviewKeyDown substitutes spaces for Tab key.
protected override void OnPreviewKeyDown(KeyEventArgs args)
{
base.OnPreviewKeyDown(args);
if (args.Source == txtbox && args.Key == Key.Tab)
{
string strInsert = new string(' ', tabspaces);
int iChar = txtbox.SelectionStart;
int iLine = txtbox.GetLineIndexFromCharacterIndex(iChar);
if (iLine != -1)
{
int iCol = iChar - txtbox.GetCharacterIndexFromLineIndex(iLine);
strInsert = new string(' ',
settingsXaml.TabSpaces - iCol % settingsXaml.TabSpaces);
}
txtbox.SelectedText = strInsert;
txtbox.CaretIndex = txtbox.SelectionStart + txtbox.SelectionLength;
args.Handled = true;
}
}
// TextBoxOnTextChanged attempts to parse XAML.
void TextBoxOnTextChanged(object sender, TextChangedEventArgs args)
{
if (IsSuspendParsing)
txtbox.Foreground = SystemColors.WindowTextBrush;
else
Parse();
}
// General Parse method also called for Reparse menu item.
void Parse()
{
StringReader strreader = new StringReader(txtbox.Text);
XmlTextReader xmlreader = new XmlTextReader(strreader);
try
{
object obj = XamlReader.Load(xmlreader);
txtbox.Foreground = SystemColors.WindowTextBrush;
if (obj is Window)
{
win = obj as Window;
statusParse.Content = "Press F7 to display Window";
}
else
{
win = null;
frameParent.Content = obj;
statusParse.Content = "OK";
}
}
catch (Exception exc)
{
txtbox.Foreground = Brushes.Red;
statusParse.Content = exc.Message;
}
}
// UnhandledException handler required if XAML object throws exception.
void DispatcherOnUnhandledException(object sender,
DispatcherUnhandledExceptionEventArgs args)
{
statusParse.Content = "Unhandled Exception: " + args.Exception.Message;
args.Handled = true;
}
}
}
Parse方法负责解析XAML。如果XamlReader.Load产生一个异常,处理函数会将TextBox的文字转成红色,并在状态栏显示错误信息。如果没有异常,则会将对象设定成Frame控件的Content。对于Window类型的对象要特别处理,将这样的对象保存在一个字段,按下F7之后会产生一个独立的窗口。
有时候,建立自XAML的element tree中有些东西会抛出异常。因为建立自XAML的对象是此应用程序的一部分,此异常可能会造成XAML Cruncher本身被结束,尽管并非XAML Cruncher本身的错误。为此,程序设置了一个UnhandledException事件处理函数,处理事件的方式是在状态栏显示此信息。除非程序可能会遇到不是自身产生的异常(像XAML Cruncher的例子这样),否则一般来说,程序不应该设置此事件处理函数。
让你改变tab空格数的菜单,会显示出一个小小的对话框。此对话框的layout是一个XAML文件。
XamlTabSpacesDialog.xaml
<!-- ======================================================
XamlTabSpacesDialog.xaml (c) 2006 by Charles Petzold
====================================================== -->
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Petzold.XamlCruncher.XamlTabSpacesDialog"
Title="Tab Spaces"
WindowStyle="ToolWindow"
SizeToContent="WidthAndHeight"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner">
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label Margin="12,12,6,12">
_Tab spaces (1-10):
</Label>
<TextBox Name="txtbox"
TextChanged="TextBoxOnTextChanged"
Margin="6,12,12,12"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Name="btnOk"
Click="OkOnClick"
IsDefault="True"
IsEnabled="False"
Margin="12">
OK
</Button>
<Button IsCancel="True"
Margin="12">
Cancel
</Button>
</StackPanel>
</StackPanel>
</Window>
如果你理解了前一章节的内容,那么此XAML文件应该没有让你意外的地方。Code-behind文件定义了public TabSpaces property和两个事件处理函数。
XamlTabSpacesDialog.cs
//----------------------------------------------------
// XamlTabSpacesDialog.cs (c) 2006 by Charles Petzold
//----------------------------------------------------
using System;
using System.Windows;
using System.Windows.Controls;
namespace Petzold.XamlCruncher
{
public partial class XamlTabSpacesDialog
{
public XamlTabSpacesDialog()
{
InitializeComponent();
txtbox.Focus();
}
public int TabSpaces
{
set { txtbox.Text = value.ToString(); }
get { return Int32.Parse(txtbox.Text); }
}
void TextBoxOnTextChanged(object sender, TextChangedEventArgs args)
{
int result;
btnOk.IsEnabled = (Int32.TryParse(txtbox.Text, out result) &&
result > 0 && result < 11);
}
void OkOnClick(object sender, RoutedEventArgs args)
{
DialogResult = true;
}
}
}
此类定义了一个名为TabSpaces的property,它直接存取TextBox的Text property。你会注意到get accessor调用Int32结构的静态Parse方法,却不担心会发生异常,这份自信是因为有TextChanged事件处理函数的缘故。只有静态的TryParse返回true,且输入的数字介于1和10之间,TextChanged事件处理函数才会enable OK按钮。
当用户从菜单选择Tab Spaces项目,XamlCruncher类会调用此对话框。下面是该菜单项的Click事件处理函数内完整的程序代码:
XamlTabSpacesDialog dlg = new XamlTabSpacesDialog();
dlg.Owner = this;
dlg.TabSpaces = settingsXaml.TabSpaces;
if ((bool)dlg.ShowDialog().GetValueOrDefault())
{
settingsXaml.TabSpaces = dlg.TabSpaces;
}
设定Owner property,可以确保XAML所指定的WindowStartupLocation发挥作用。当用户按下Cancel关闭对话框,程序却存取TabSpaces property,就可能会造成麻烦发生。只有在按下OK按钮时,TabSpaces才保证不会发生异常。
改变TextBox与Frame方向(orientation)的菜单项,具有自己的类,以将4种可行方向用符号表示,并附带图片。当用户选取新的方向时,此类也必须重新安排Grid中element的位置。
XamlOrientationMenuItem.cs
//--------------------------------------------------------
// XamlOrientationMenuItem.cs (c) 2006 by Charles Petzold
//--------------------------------------------------------
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace Petzold.XamlCruncher
{
class XamlOrientationMenuItem : MenuItem
{
MenuItem itemChecked;
Grid grid;
TextBox txtbox;
Frame frame;
// Orientation public property of type Dock.
public Dock Orientation
{
set
{
foreach (MenuItem item in Items)
if (item.IsChecked = (value == (Dock)item.Tag))
itemChecked = item;
}
get
{
return (Dock)itemChecked.Tag;
}
}
// Constructor requires three arguments.
public XamlOrientationMenuItem(Grid grid, TextBox txtbox, Frame frame)
{
this.grid = grid;
this.txtbox = txtbox;
this.frame = frame;
Header = "_Orientation";
for (int i = 0; i < 4; i++)
Items.Add(CreateItem((Dock)i));
(itemChecked = (MenuItem) Items[0]).IsChecked = true;
}
// Create each menu item based on Dock setting.
MenuItem CreateItem(Dock dock)
{
MenuItem item = new MenuItem();
item.Tag = dock;
item.Click += ItemOnClick;
item.Checked += ItemOnCheck;
// Two text strings that appear in menu item.
FormattedText formtxt1 = CreateFormattedText("Edit");
FormattedText formtxt2 = CreateFormattedText("Display");
double widthMax = Math.Max(formtxt1.Width, formtxt2.Width);
// Create a DrawingVisual and a DrawingContext.
DrawingVisual vis = new DrawingVisual();
DrawingContext dc = vis.RenderOpen();
// Draw boxed text on the visual.
switch (dock)
{
case Dock.Left: // Edit on left, display on right.
BoxText(dc, formtxt1, formtxt1.Width, new Point(0, 0));
BoxText(dc, formtxt2, formtxt2.Width,
new Point(formtxt1.Width + 4, 0));
break;
case Dock.Top: // Edit on top, display on bottom.
BoxText(dc, formtxt1, widthMax, new Point(0, 0));
BoxText(dc, formtxt2, widthMax,
new Point(0, formtxt1.Height + 4));
break;
case Dock.Right: // Edit on right, display on left.
BoxText(dc, formtxt2, formtxt2.Width, new Point(0, 0));
BoxText(dc, formtxt1, formtxt1.Width,
new Point(formtxt2.Width + 4, 0));
break;
case Dock.Bottom: // Edit on bottom, display on top.
BoxText(dc, formtxt2, widthMax, new Point(0, 0));
BoxText(dc, formtxt1, widthMax,
new Point(0, formtxt2.Height + 4));
break;
}
dc.Close();
// Create Image object based on Drawing from visual.
DrawingImage drawimg = new DrawingImage(vis.Drawing);
Image img = new Image();
img.Source = drawimg;
// Set the Header of the menu item to the Image object.
item.Header = img;
return item;
}
// Handles the hairy FormattedText arguments.
FormattedText CreateFormattedText(string str)
{
return new FormattedText(str, CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface(SystemFonts.MenuFontFamily, SystemFonts.MenuFontStyle,
SystemFonts.MenuFontWeight, FontStretches.Normal),
SystemFonts.MenuFontSize, SystemColors.MenuTextBrush);
}
// Draws text surrounded by a rectangle.
void BoxText(DrawingContext dc, FormattedText formtxt,
double width, Point pt)
{
Pen pen = new Pen(SystemColors.MenuTextBrush, 1);
dc.DrawRectangle(null, pen,
new Rect(pt.X, pt.Y, width + 4, formtxt.Height + 4));
double X = pt.X + (width - formtxt.Width) / 2;
dc.DrawText(formtxt, new Point(X + 2, pt.Y + 2));
}
// Check and uncheck items when clicked.
void ItemOnClick(object sender, RoutedEventArgs args)
{
itemChecked.IsChecked = false;
itemChecked = args.Source as MenuItem;
itemChecked.IsChecked = true;
}
// Change the orientation based on the checked item.
void ItemOnCheck(object sender, RoutedEventArgs args)
{
MenuItem itemChecked = args.Source as MenuItem;
// Initialize the 2nd and 3rd rows and columns to zero.
for (int i = 1; i < 3; i++)
{
grid.RowDefinitions[i].Height = new GridLength(0);
grid.ColumnDefinitions[i].Width = new GridLength(0);
}
// Initialize the cell of the TextBox and Frame to zero.
Grid.SetRow(txtbox, 0);
Grid.SetColumn(txtbox, 0);
Grid.SetRow(frame, 0);
Grid.SetColumn(frame, 0);
// Set row and columns based on the orientation setting.
switch ((Dock)itemChecked.Tag)
{
case Dock.Left: // Edit on left, display on right.
grid.ColumnDefinitions[1].Width = GridLength.Auto;
grid.ColumnDefinitions[2].Width =
new GridLength(100, GridUnitType.Star);
Grid.SetColumn(frame, 2);
break;
case Dock.Top: // Edit on top, display on bottom.
grid.RowDefinitions[1].Height = GridLength.Auto;
grid.RowDefinitions[2].Height =
new GridLength(100, GridUnitType.Star);
Grid.SetRow(frame, 2);
break;
case Dock.Right: // Edit on right, display on left.
grid.ColumnDefinitions[1].Width = GridLength.Auto;
grid.ColumnDefinitions[2].Width =
new GridLength(100, GridUnitType.Star);
Grid.SetColumn(txtbox, 2);
break;
case Dock.Bottom: // Edit on bottom, display on top.
grid.RowDefinitions[1].Height = GridLength.Auto;
grid.RowDefinitions[2].Height =
new GridLength(100, GridUnitType.Star);
Grid.SetRow(txtbox, 2);
break;
}
}
}
}
XamlCruncher类的构造函数也会存取顶层的Help菜单项,并加入一个Help子菜单。此菜单项显示一个窗口,内容包含对程序和新菜单项的简单描述。这些年来,Help文件可能用Rich Text Format(RTF)格式保存,或者用流行的HTML格式保存。
然而,让我们展示一下新技术,使用一个FlowDocument对象来写Help文件,RichTextBox内部正是使用FlowDocument对象。我用更早期版本的XAML Cruncher写出下面的文件。这个文件应该相当容易理解,因为里面都是完整的英文单词,像是Paragraph、Bold、Italic。
XamlCruncherHelp.xaml
<!-- ===================================================
XamlCruncherHelp.xaml (c) 2006 by Charles Petzold
=================================================== -->
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
TextAlignment="Left">
<Paragraph TextAlignment="Center" FontSize="32" FontStyle="Italic"
LineHeight="24">
XAML Cruncher
</Paragraph>
<Paragraph TextAlignment="Center">
© 2006 by Charles Petzold
</Paragraph>
<Paragraph FontSize="16pt" FontWeight="Bold" LineHeight="16">
Introduction
</Paragraph>
<Paragraph>
XAML Cruncher is a sample program from Charles Petzold's book
<Italic>
Applications = Code + Markup:
A Guide to the Microsoft Windows Presentation Foundation
</Italic>
published by Microsoft Press in 2006.
XAML Cruncher provides a convenient way to learn about and experiment
with XAML, the Extensible Application Markup Language.
</Paragraph>
<Paragraph>
XAML Cruncher consists of an Edit section (in which you enter and edit
a XAML document) and a Display section that shows the object created
from the XAML. If the XAML document has errors, the text is displayed
in red and the status bar indicates the problem.
</Paragraph>
<Paragraph>
Most of the interface and functionality of the edit section of
XAML Cruncher is based on Windows Notepad.
The <Bold>Xaml</Bold> menu provides additional features.
</Paragraph>
<Paragraph FontSize="16pt" FontWeight="Bold" LineHeight="16">
Xaml Menu
</Paragraph>
<Paragraph>
The <Bold>Orientation</Bold> menu item lets you choose whether you
want the Edit and Display sections of XAML Cruncher arranged
horizontally or vertically.
</Paragraph>
<Paragraph>
The <Bold>Tab Spaces</Bold> menu item displays a dialog box that lets
you choose the number of spaces you want inserted when you press the
Tab key. Changing this item does not change any indentation
already in the current document.
</Paragraph>
<Paragraph>
There are times when your XAML document will be so complex that it
takes a little while to convert it into an object. You may want to
<Bold>Suspend Parsing</Bold> by checking this item on the
<Bold>Xaml</Bold> menu.
</Paragraph>
<Paragraph>
If you've suspended parsing, or if you want to reparse the XAML file,
select <Bold>Reparse</Bold> from the menu or press F6.
</Paragraph>
<Paragraph>
If the root element of your XAML is <Italic>Window</Italic>,
XAML Cruncher will not be able to display the <Italic>Window</Italic>
object in its own window.
Select the <Bold>Show Window</Bold> menu item or press F7 to view
the window.
</Paragraph>
<Paragraph>
When you start up XAML Cruncher (and whenever you select
<Bold>New</Bold> from the <Bold>File</Bold> menu), the Edit window
displays a simple startup document.
If you want to use the current document as the startup document,
select the <Bold>Save as Startup Document</Bold> item.
</Paragraph>
</FlowDocument>
此文件必须在Visual Studio中被指定为资源Resource。Visual Studio默认会将它指定成Page。它必须是Resource,因为XamlCruncher的程序代码是以资源的方式对待它的。HelpOnClick事件处理函数先为此资源取得一个URI对象,然后建立一个Stream:
Uri uri = new Uri("pack://application:,,,/XamlCruncherHelp.xaml");
Stream stream = Application.GetResourceStream(uri).Stream;
此方法然后建立一个Window,设定Title,将Content property设定成XamlReader.Load所返回来的FlowDocument对象(传入引用此资源的Stream对象):
Window win = new Window();
win.Title = "XAML Cruncher Help";
win.Content = XamlReader.Load(stream);
win.Show();
有一些其他的做法。其中一种是建立一个Frame控件,并将其Source property直接设定为此Uri对象:
Frame frame = new Frame();
frame.Source = new Uri("pack://application:,,/XamlCruncherHelp.xaml");
现在,建立一个Window,将其内容设定为此Frame:
Window win = new Window();
win.Title = "XAML Cruncher Help";
win.Content = frame;
win.Show();
第三种做法:首先,建立一个XAML文件,用来定义此Help窗口。可以将此文件命名为XamlHelpDialog.xaml:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="XAML Cruncher Help"
x:Class="Petzold.XamlCruncher.XamlHelpDialog">
<Frame Source="XamlCruncherHelp.xaml" />
</Window>
请注意用来设定Frame控件Source property的简化语法。现在HelpOnClick方法减少到只剩下这3条语句:
XamlHelpDialog win = new XamlHelpDialog();
win.InitializeComponent();
win.Show();
在建立XAML所定义的XamlHelpDialog对象之后,此方法会调用InitializeComponent(这个工作一般是由code-behind文件负责的)以及Show。此做法有趣的一点在于,负责定义FlowDocument的XamlCruncherHelp.xaml的Build Action可以是Resource,也可以是Page。如果是Page的话,此XAML会以编译过的BAML文件形式,放在.EXE文件中。
不管你如何显示Help文件,XAML Cruncher现在已经完全具备一切,而且可以使用了。在本章和后续的许多章节中,我会向你展示可以用XAML Cruncher(或类似的程序)建立并执行的独立XAML文件。
让我们从一个简单的XAML文件开始,这是XAML Cruncher默认建立的内容:
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml>
Hello, XAML!
</Button>
此静态的XamlReader.Load方法解析此文字内容,并建立Button类型的对象。为了方便起见,我会将XamlReader.Load方法称为解析器(parser),因为它解析XAML并以此建立一个或多个对象。
程序员可能问的第一个问题是:解析器怎么知道要使用什么类来建立此特定的Button对象?毕竟,.NET有3种Button类。有Windows Forms的Button和ASP.NET的Button。虽然此XAML文件包含两个XML命名空间,它并没有包含像是System.Windows.Controls这样的CLR命名空间。此XAML也没有包含对PresentationFramework.dll组件的引用(此组件定义了System.Windows.Controls.Button类)。为何解析器不要求提供完整名称(含命名空间)的类或using指示符呢?
其中一种答案是:WPF应用程序没有引用Windows Forms组件或ASP.NET组件,而是引用了PresentationFramework.dll组件,因为此组件是定义XamlReader类的组件。但是就算WPF应用程序引用了System.Windows.Forms.dll或System.Web.dll,而且这些组件被此应用程序所加载,此解析器仍然知道该使用哪个Button类。
此迷团的解答就在PresentationFramework组件内。此组件包含许多自定义attribute。(应用程序可以调用此Assembly对象的GetCustomAttributes,查询出这些attribute。)有数个attribute是属于XmlnsDefinitionAttribute,而且此类包含两个重要的property,名为XmlNamespace与ClrNamespace。PresentationFramework的一个XmlnsDefinitionAttribute对象将XmlNamespace设定成字符串“http://schemas.microsoft.com/winfx/2006/xaml/presentation”,并且将ClrNamespace property设定成字符串“System.Windows.Controls”。语法是这样的:
[assembly:XmlnsDefinition
("http://schemas.microsoft.com/winfx/2006/xaml/presentation",
"System.Windows.Controls")]
其他的XmlnsDefinition attribute将此相同的XML命名空间与其他CLR命名空间建立关联,这些CLR命名空间包括了System.Windows、System.Windows.Controls.Primitives、System.Windows.Input、System.Windows.Shapes等。
此XAML解析器会检查应用程序所加载的所有组件的XmlnsDefinition attribute(如果有的话)。如果这些attribute内的XML命名空间符合XAML文件内的XML命名空间,当在这些组件中搜寻Button类时,解析器就会确定是哪个CLR命名空间。
如果程序引用了另外的组件,而该组件包含一个类似的XmlnsDefinition,其XML命名空间一样,但却是完全不同的Button类,那么此解析器应该会遇到问题。但是这不可能发生,除非某人建立一个组件,使用微软的XML命名空间或微软内部有人犯了大错。
让我们设定Width property,为XAML Cruncher内的button指定一个明确的宽度:
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml
Width="144">
Hello, XAML!
</Button>
此解析器可以通过reflection得知Button需要Width property。此解析器也可以得知此property是double类型,或者CLR类型的System.Double。然而,对于XML attribute来说,Width的值是一个字符串。此解析器必须将该字符串转换为double类型的对象。这听起来好像没什么,事实上,Double结构就包含了一个静态的Parse方法,可以将字符串转成数字。
然而,一般情况下,此转换并不是简单的工作,特别是,你也可以在指定宽度时使用英寸当单位:
Width="1.5in"
你可以在数字和“in”之间放上空格,而且还可以使用大写或小写字母:
Width="1.5 IN"
也可以使用科学记数法:
Width="15e-1in"
指定非数字(Not a Number)时用NaN(要遵照这样的大小写):
Width="NaN"
虽然语义上不适合Width property,但某些double attribute允许“Infinity”或“-Infinity”。当然,你也可以采用公制:
Width="3.81cm"
或者,如果你具有印刷的知识背景,你可以使用打印机的point:
Width="108pt"
Double.Parse方法允许科学记数法、NaN、Infinity,但是不允许“in”、“cm”、或“pt”,这些必须另外处理。
当XAML解析器遇到double类型的property,会从System.ComponentModel命名空间找出DoubleConverter类。这是诸多的转换器(converter)类之一。它们全都是继承自TypeConverter,而且包含一个名为ConvertFromString的方法,该方法内部可能(以此例来说)会使用Double.Parse方法来进行转换。
类似地,当你设定Margin attribute(类型为Thickness)的时候,此解析器会找出System.Windows命名空间的ThicknessConverter类。此转换器允许你设定一个值,同时用在4边:
Margin="48"
或者指定两个值,第一个用在左右,第二个用在上下:
Margin="48 96"
你可以将这两个数字用一个空白或逗号分开。你也可以使用4个数字,分别表示左、上、右、下:
Margin="48 96 24 192"
如果你使用“in”或“cm”或“pt”,那么数字和单位之间不可以有空格:
Margin="1.27cm 96 18pt 2in"
当你在Visual Studio的编辑器中输入XAML时,它会应用某些比实际的解析器更加严格的规则,如果你的XAML不符合规则,就会警告你。比方说,定义Thickness对象,Visual Studio希望你用逗号分开这些值。
如果是布尔值,使用“true”或“false”,随便你大小写怎么混用:
IsEnabled="FaLSe"
然而Visual Studio希望你使用“True”和“False”。
对于值必须为枚举成员的property来说,EnumConverter类要求你在设定attribute时,使用枚举成员本身(不需冠以枚举名称):
HorizontalAlignment="Center"
你不要将FontStretch、FontStyle、FontWeight property设定成枚举成员,而要将它们设定成FontStretches、FontStyles、FontWeights等静态类的property。FontStretchConverter、FontStyleConverter、FontWeightConverter类让你直接使用那些静态的property。你将FontFamily设定成一个字符串,将FontSize设定成一个double:
FontFamily="Times New Roman"
FontSize="18pt"
FontWeight="Bold"
FontStyle="Italic"
让我们再来做一些不一样的事。下面的XAML文件名为Star.xaml,呈现出一个五角星形:
Star.xaml
<!-- =======================================
Star.xaml (c) 2006 by Charles Petzold
======================================= -->
<Polygon xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Points="144 48, 200 222, 53 114, 235 114, 88 222"
Fill="Red"
Stroke="Blue"
StrokeThickness="5" />
Polygon包含一个名为Points的property,类型为PointCollection。幸好有PointCollectionConverter的存在,你可以将这些点指定为一连串的X坐标和Y坐标,这些数字可以通过空格或逗号分隔。某些人在X和Y坐标之间用逗号分隔,在点和点之间用空格分隔,有些人(包括我)喜欢用逗号分隔点。
BrushConverter类让你使用Brushes类的静态成员来指定颜色,当然,你也可以使用十六进制的RGB颜色值:
Fill="#FF0000"
下面是相同的颜色,但是alpha channel为128(也就是半透明):
Fill="#80FF0000"
你也可以使用scRGB分量的红、绿、蓝颜色值,alpha channel置于最前面。半透明的红表达如下:
Fill="sc#0.5,1,0,0"
现在,不将Fill property设定成SolidColorBrush类型的对象,让我们将Fill property设定成LinearGradientBrush对象。
忽然间,我们好像撞到墙壁了。我们要如何用文字字符串表达整个LinearGradientBrush,以便指定给Fill property?SolidColorBrush只需要一个颜色值,但渐变画刷(gradient brush)需要至少两个颜色值和两个渐变点。标记(markup)的限制似乎已经出现了。
你确实可以在XAML中指定LinearGradientBrush,为了要了解它是如何完成的,让我们先看看另一种语法,也可以将Fill property设定成红色实心画刷。首先,将没有内容的Polygon对象empty tag,用明确的end tag取代:
<Polygon xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Points="144 48, 200 222, 53 114, 235 114, 88 222"
Fill="Red"
Stroke="Blue"
StrokeThickness="5">
</Polygon>
接下来,将Fill attribute从Polygon tag中移除,用名为Polygon.Fill的child element取而代之。该element的内容是“Red”:
<Polygon xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Points="144 48, 200 222, 53 114, 235 114, 88 222"
Stroke="Blue"
StrokeThickness="5">
<Polygon.Fill>
Red
</Polygon.Fill>
</Polygon>
让我们将某些术语固定下来。许多XAML element关系到类和结构,并且导致对象的建立。这些element称为object element:
<Polygon ... />
Element常常会包含attribute,以设定这些对象的property。这些attribute称为property attribute:
Fill="Red"
用涉及child element的语法来指定property也行。这称为property element:
<Polygon.Fill>
Red
</Polygon.Fill>
Property element的特征是在element名称和property名称之间有一个点。Property element没有attribute。你绝对不会看到下面的描述方式:
<!-- Wrong Syntax! -->
<Polygon.Fill SomeAttribute="Whatever">
...
</Polygon.Fill>
甚至连XML命名空间的声明也不能出现在这里。如果你试着在XAML Cruncher中对一个property element设定attribute,你会得到信息“无法在property element中设定property”,这是当你尝试这样做时XamlReader.Load所抛出异常的信息。
Property element的内容必须可以被转成此property的类型。Polygon.Fill property element是Fill property,类型是Brush,所以此property element的内容必须可以被转成Brush:
<Polygon.Fill>
Red
</Polygon.Fill>
这样也行:
<Polygon.Fill>
#FF0000
</Polygon.Fill>
你可以用下面的语法(要打比较多的字),让Polygon.Fill的内容更明确是Brush:
<Polygon.Fill>
<Brush>
Red
</Brush>
</Polygon.Fill>
现在Polygon.Fill property element的内容是一个Brush object element,也就是文字字符串“Red”。此内容可以被转成SolidColorBrush类型的对象,所以你可以将Polygon.Fill property element写成这样:
<Polygon.Fill>
<SolidColorBrush>
Red
</SolidColorBrush>
</Polygon.Fill>
SolidColorBrush具有一个名为Color的property,而且ColorConverter类允许和Brush转换器(converter)相同的转换,所以你可以使用一个property attribute设定SolidColorBrush的Color property:
<Polygon.Fill>
<SolidColorBrush Color="Red">
</SolidColorBrush>
</Polygon.Fill>
然而,你现在无法使用Brush取代SolidColorBrush,因为Brush不具有名为Color的property。
因为SolidColorBrush不具有content,你可以使用empty-element语法写tag:
<Polygon.Fill>
<SolidColorBrush Color="Red" />
</Polygon.Fill>
或者,你可以使用property element语法,将SolidColorBrush的Color property分离出来:
<Polygon.Fill>
<SolidColorBrush>
<SolidColorBrush.Color>
Red
</SolidColorBrush.Color>
</SolidColorBrush>
</Polygon.Fill>
SolidColorBrush类的Color property是Color类型的对象,所以你可以明确地使用一个object element作为SolidColorBrush.Color的内容:
<Polygon.Fill>
<SolidColorBrush>
<SolidColorBrush.Color>
<Color>
Red
</Color>
</SolidColorBrush.Color>
</SolidColorBrush>
</Polygon.Fill>
Color有4个property,分别名为A、R、G、B,都是byte类型。你可以在Color tag中设定这些,不管是用十进制或十六进制的语法都行:
<Polygon.Fill>
<SolidColorBrush>
<SolidColorBrush.Color>
<Color A="255" R="#FF" G="0" B="0">
</Color>
</SolidColorBrush.Color>
</SolidColorBrush>
</Polygon.Fill>
别忘了,你无法在SolidColorBrush.Color tag中设定这4个peoperty,因为你“无法在property element中设定property”,还记得这个异常信息吧!
因为Color element现在没有内容,你可以用empty-element的语法写它:
<Polygon.Fill>
<SolidColorBrush>
<SolidColorBrush.Color>
<Color A="255" R="#FF" G="0" B="0" />
</SolidColorBrush.Color>
</SolidColorBrush>
</Polygon.Fill>
或者,你可以将Color的一个或多个attribute分离出来:
<Polygon.Fill>
<SolidColorBrush>
<SolidColorBrush.Color>
<Color A="255" G="0" B="0">
<Color.R>
#FF
</Color.R>
</Color>
</SolidColorBrush.Color>
</SolidColorBrush>
</Polygon.Fill>
Color的R property类型是Byte,这是定义在System命名空间中的结构。你甚至可以在XAML中使用Byte element,让R的数据类型更明显。然而,System命名空间和XAML文件顶端的两个XML命名空间都没有关联,为了要使用Byte结构,你需要另一个XML命名空间的声明。让我们将System命名空间和s前缀产生关联:
xmlns:s="clr-namespace:System;assembly=mscorlib"
请注意,引号内的字符串一开始是clr-namespace,后面接着一个冒号和一个CLR命名空间,彷佛你正在将此前缀和程序中的一个CLR命名空间关联起来。(如同第19章UseCustomClass项目所展示的那样。)因为System命名空间的类与结构是位于外部组件(external assembly),此信息相当重要,需要跟在后面。在CLR命名空间之后,加上一个分号,然后使用“assembly”这个字,再用一个等号,最后是组件名称。请注意,冒号是用来分隔clr-namespace和CLR命名空间的,但是等号是用来分隔assembly和组件名称的。这里的概念是,冒号之前的开头部分,可以比拟为传统命名空间声明的“http:”部分。
该声明需要放进Byte element本身,或者放到Byte element的父亲。让我们将它放在Color element中(这么做的理由,稍后你就知道了):
<Polygon.Fill>
<SolidColorBrush>
<SolidColorBrush.Color>
<Color xmlns:s="clr-namespace:System;assembly=mscorlib"
A="255" G="0" B="0">
<Color.R>
<s:Byte>
#FF
</s:Byte>
</Color.R>
</Color>
</Soli

