Chapter 20. Properties and Attributes

The XamlReader.Load method that you encountered in the last chapter might have suggested a handy programming tool to you. Suppose you have a TextBox and you attach a handler for the TextChanged event. As you type XAML into that TextBox, the TextChanged event handler could try passing that XAML to the XamlReader.Load method and display the resultant object. You'd need to put the call to XamlReader.Load in a try block because most of the time the XAML will be invalid while it's being entered, but such a programming tool would potentially allow immediate feedback of your experimentations with XAML. It would be a great tool for learning XAML and fun as well.

That's the premise behind the XAML Cruncher program. It's certainly not the first program of its type, and it won't be the last. XAML Cruncher is build on Notepad Clone. As you'll see, XAML Cruncher replaces the TextBox that fills Notepad Clone's client area with a Grid. The Grid contains the TextBox in one cell and a Frame control in another with a GridSplitter in between. When the XAML you type into the TextBox is successfully converted into an object by XamlReader.Load, that object is made the Content of the Frame.

The XamlCruncher project includes every file in the NotepadClone project except for NotepadCloneAssemblyInfo.cs. That file is replaced with this one:

XamlCruncherAssemblyInfo.cs

[View full width]//--------- ------------------------------------------------ // 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")]


As you'll recall, the NotepadCloneSettings class contained several items saved as user preferences. The XamlCruncherSettings class inherits from NotepadCloneSettings and adds just three items. The first is named Orientation and it governs the orientation of the TextBox and the Frame. XAML Cruncher has a menu item that lets you put one on top of the other or have them side by side. Also, XAML overrides the normal TextBox handling of the Tab key and inserts spaces instead. The second user preference is the number of spaces inserted when you press the Tab key.

The third user preference is a string containing some simple XAML that shows up in the TextBox when you first run the program or when you select New from the File command. A menu item lets you set the current contents of the TextBox as this startup document item.

XamlCruncherSettings.cs

[View full width]//--------- -------------------------------------------- // 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; } } }


In addition, the XamlCruncherSettings constructor changes the default font to a 10-point, fixed-pitch, Lucida Console. Of course, once you run XAML Cruncher, you can change the font to whatever you want.

Here's the XamlCruncher class that derives from the NotepadClone class. This class is responsible for creating the Grid that becomes the new content of the Window, as well as for creating the top-level menu item labeled "Xaml" and the six items on its submenu.

XamlCruncher.cs

[View full width]//--------------------------------------------- // 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; } } }


The Parse method toward the bottom of the class is responsible for parsing the XAML. If XamlReader.Load raises an exception, the handler turns the text of the TextBox red and displays the exception message in the status bar. Otherwise, it sets the object to the Content of the Frame control. Special handling exists for an object of type Window. That's saved as a field to await the pressing of F7 to launch it as a separate window.

Sometimes something in the element tree created from the XAML throws an exception. Because the object created from the XAML is part of the application, this exception could cause XAML Cruncher itself to be terminated through no fault of its own. For that reason, the program installs an UnhandledException event handler and processes the event by displaying the message in the status bar. In general, programs shouldn't install this event handler unless (like XAML Cruncher) they may encounter exceptions that are not related to buggy program code.

The menu item that lets you change the number of tab spaces displays a small dialog box. The layout of this dialog box is a XAML file.

XamlTabSpacesDialog.xaml

[View full width]<!-- === =================================================== 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>


This XAML file shouldn't contain any surprises if you've assimilated the contents of the previous chapter. The code-behind file defines the public TabSpaces property and two event handlers.

XamlTabSpacesDialog.cs

[View full width]//---------------------------------------------------- // 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; } } }


The class defines a property named TabSpaces that directly accesses the Text property of the TextBox. You'll notice that the get accessor calls the static Parse method of the Int32 structure with full confidence that it won't raise an exception. That confidence is a result of the TextChanged event handler, which doesn't enable the OK button until the static TryParse returns true and the entered number isn't less than 1 or greater than 10.

The XamlCruncher class invokes this dialog box when the user selects the Tab Spaces item from the menu. Here are the entire contents of the Click event handler for that menu item:

XamlTabSpacesDialog dlg = new XamlTabSpacesDialog(); dlg.Owner = this; dlg.TabSpaces = settingsXaml.TabSpaces; if ((bool)dlg.ShowDialog().GetValueOrDefault()) { settingsXaml.TabSpaces = dlg.TabSpaces; }

Setting the Owner property ensures that the WindowStartupLocation specified in the XAML file works. This is one dialog box where it's problematic if the TabSpaces property is accessed when the user dismisses the dialog box by clicking the Cancel button. The TabSpaces property is only guaranteed not to raise an exception if the user clicks the OK button.

The menu item to change the orientation of the TextBox and the Frame has its own class that symbolizes the four available orientations with little pictures. The class must also rearrange the elements on the Grid when the user selects a new orientation.

XamlOrientationMenuItem.cs

[View full width]//--------- ----------------------------------------------- // 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; } } } }


The constructor of the XamlCruncher class also accesses the top-level Help menu item and adds a subitem of Help. This item displays a window that contains a short description of the program and the new menu items. In years gone by, this Help file might have been stored in the Rich Text Format (RTF) or that quaint but popular markup language, HTML.

However, let's demonstrate a commitment to new technologies and write the help file as a FlowDocument object, which is the type of object created by the RichTextBox. I hand-coded the following file in an early version of XAML Cruncher. It should be fairly self-explanatory because the elements are full words, such as Paragraph, Bold, and Italic.

XamlCruncherHelp.xaml

[View full width]<!-- === ================================================ 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"> &#x00A9; 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>


This file must be designated as a Resource in the XamlCruncher project in Microsoft Visual Studio. Visual Studio will want to make it a Page. It must be a Resource because that's how the XamlCruncher code treats it. The HelpOnClick event handler first obtains a URI object for the resource and creates a Stream:

Uri uri = new Uri("pack://application:,,,/XamlCruncherHelp.xaml"); Stream stream = Application.GetResourceStream(uri).Stream;

The method then creates a Window, sets the Title, and sets the Content property to the FlowDocument object that XamlReader.Load returns when passed the Stream object that references the resource:

Window win = new Window(); win.Title = "XAML Cruncher Help"; win.Content = XamlReader.Load(stream); win.Show();

There are a couple of alternatives to this approach. One possibility is to create a Frame control and set its Source property directly to the Uri object:

Frame frame = new Frame(); frame.Source = new Uri("pack://application:,,/XamlCruncherHelp.xaml");

Now create the Window and set its content to the Frame:

Window win = new Window(); win.Title = "XAML Cruncher Help"; win.Content = frame; win.Show();

A third approach: First, create a XAML file defining the Help window. Call it XamlHelpDialog.xaml, perhaps:

<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>

Notice the simplified syntax for setting the Source property of the Frame control. Now the HelpOnClick method reduces to the following three statements:

XamlHelpDialog win = new XamlHelpDialog(); win.InitializeComponent(); win.Show();

After creating the XamlHelpDialog object defined in the XAML, the method calls InitializeComponent (a job normally performed by a code-behind file) and then Show. One interesting aspect of this approach is that XamlCruncherHelp.xaml defining the FlowDocument can have a Build Action of either Resource or Page. In the latter case, the XAML file is stored in the .EXE file as a compiled BAML file.

Regardless of how you display the Help file, XAML Cruncher is now complete and ready to use. For much of this chapter and many of the chapters that follow I'll be showing you stand-alone XAML files that you can create and run in XAML Cruncher or any equivalent program.

Let's begin with the simple XAML file that XAML Cruncher creates by default:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml> Hello, XAML! </Button>

The static XamlReader.Load method parses this text and creates an object of type Button. For convenience, I will be referring to the XamlReader.Load method as the parser because it parses the XAML and creates one or more objects from it.

This first question that might occur to a programmer is this: How does the parser know which class to use to create this particular Button object? After all, there are three different Button classes in .NET. There's a Button in Windows Forms and another you use with ASP.NET (Active Server Pages). Although this XAML file contains two XML namespaces, it doesn't contain a Common Language Runtime namespace such as System.Windows.Controls. Nor does the XAML contain any references to the assembly PresentationFramework.dll in which the System.Windows.Controls.Button class resides. Why doesn't the parser require a fully qualified class name or something akin to a using directive?

One answer is that a WPF application probably doesn't have references to the Windows Forms assemblies or the ASP.NET assemblies in which the other Button classes reside, but it definitely has a reference to the PresentationFramework.dll assembly because that's where the XamlReader class is located. But even if a WPF application had references to System.Windows.Forms.dll or System.Web.dll, and these assemblies were loaded by the application, the parser still knows which Button class to use.

The solution to this mystery lies inside the PresentationFramework assembly. This assembly contains a number of custom attributes. (An application can interrogate these attributes by calling GetCustomAttributes on the Assembly object.) Several of these attributes are of type XmlnsDefinitionAttribute, and this class contains two properties of importance named XmlNamespace and ClrNamespace. One of the XmlnsDefinitionAttribute objects in PresentationFramework has its XmlNamespace set to the string "http://schemas.microsoft.com/winfx/2006/xaml/presentation" and its ClrNamespace property set to the string "System.Windows.Controls." The syntax looks like this:

[assembly:XmlnsDefinition ("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Controls")]

Other XmlnsDefinition attributes associate this same XML namespace with the CLR namespaces System.Windows, System.Windows.Controls.Primitives, System.Windows.Input, System.Windows.Shapes, and so forth.

The XAML parser examines the XmlnsDefinition attributes (if any) in all the assemblies loaded by the application. If any of the XML namespaces in these attributes match the XML namespace in the XAML file, the parser knows which CLR namespaces to assume when searching for a Button class in these assemblies.

The parser would certainly have a problem if the program referenced another assembly that contained a similar XmlnsDefinition attribute with the same XML namespace but a completely different Button class. But that really can't happen unless someone's created an assembly using Microsoft's XML namespace or somebody at Microsoft goofs up big time.

Let's give the button in XAML Cruncher an explicit width by setting the Width property:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml Width="144"> Hello, XAML! </Button>

The parser can determine from reflection that Button indeed has a Width property. The parser can also determine that this property is of type double, or the CLR type System.Double. However, as with every XML attribute, the value of the Width is a string. The parser must convert that string into an object of type double. This sounds fairly trivial. In fact, the Double structure includes a static Parse method for the express purpose of converting strings to numbers.

In the general case, however, this conversion is not trivial, particularly considering that you can also specify that same width in inches like this:

Width="1.5in"

You can have a space between the number and the "in" string, and it can be spelled in uppercase or lowercase:

Width="1.5 IN"

Scientific notation is allowed:

Width="15e-1in"

So is specifying Not a Number (and case matters here):

Width="NaN"

Although not semantically proper for the Width property, some double attributes allow "Infinity" or "-Infinity." You can also go metric, of course:

Width="3.81cm"

Or, if you have a typographical background, you can use printer's points:

Width="108pt"

The Double.Parse method allows scientific notation, NaN, and Infinity, but not the "in," "cm," or "pt" strings. Those must be handled elsewhere.

When the XAML parser encounters a property of type double, it locates a class named DoubleConverter from the System.ComponentModel namespace. This is one of many "converter" classes. They all derive from TypeConverter and include a method named ConvertFromString that ultimately (in this case) probably makes use of the Double.Parse method to perform conversion.

Similarly, when you set a Margin attribute (of type Thickness), the parser locates the ThicknessConverter class in the System.Windows namespace. This converter allows you to set a single value that applies to all four sides:

Margin="48"

or two values where the first applies to the left and right and the second applies to the top and bottom:

Margin="48 96"

You can separate these two numbers with a space or a comma. You can also use four numbers for the left, top, right, and bottom:

Margin="48 96 24 192"

If you want to use "in," or "cm," or "pt" here, there can't be a space between the number and the measurement string:

Margin="1.27cm 96 18pt 2in"

When you enter XAML in Visual Studio's editor, it applies some more stringent rules than the actual parser and displays warning messages if your XAML does not comply. For defining a Thickness object, Visual Studio prefers that commas separate the values.

For Boolean values, use "true" or "false" with whatever mix of case you want:

IsEnabled="FaLSe"

Visual Studio, however, prefers "True" and "False."

For properties that you set to members of an enumeration, the EnumConverter class requires that you use the enumeration member by itself when setting the attribute:

HorizontalAlignment="Center"

As you'll recall, you set the FontStretch, FontStyle, and FontWeight properties not to enumeration members but to static properties of the FontStretches, FontStyles, and FontWeights classes. The FontStretchConverter, FontStyleConverter, and FontWeightConverter classes let you use those static properties directly. You set FontFamily to a string, and FontSize to a double:

FontFamily="Times New Roman" FontSize="18pt" FontWeight="Bold" FontStyle="Italic"

Let's move on to something different. This is a XAML file named Star.xaml that is rendered as a five-pointed star:

Star.xaml

[View full width]<!-- ======================================= 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 contains a property named Points of type PointCollection. Fortunately a PointCollectionConverter exists that lets you specify the points as a series of alternating X and Y coordinates. The numbers can be separated with either spaces or commas. Some people put commas between the X and Y coordinates of each point and use spaces to separate the points. Others (including me) prefer using commas to separate the points.

The BrushConverter class lets you specify colors using static members of the Brushes class, of course, but you can also use hexadecimal RGB color values:

Fill="#FF0000"

The following is the same color but has an alpha channel of 128 (half transparent):

Fill="#80FF0000"

You can also use fractional red, green, and blue values in the scRGB color scheme, preceded by the alpha channel. This is half-transparent red:

Fill="sc#0.5,1,0,0"

Now instead of setting the Fill property to an object of type SolidColorBrush, let's set the Fill property to a LinearGradientBrush.

And suddenly we seem to hit a wall. How can you possibly represent an entire LinearGradientBrush in a text string that you assign to the Fill property? The SolidColorBrush requires just one color value, while gradient brushes require at least two colors as well as gradient stops. The limitations of markup have now been revealed.

You can indeed specify a LinearGradientBrush in XAML, and to understand how it's done, let's first look at an alternative syntax for setting the Fill property to a solid red brush. First, replace the empty content tag of the Polygon object with an explicit 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>

Now remove the Fill attribute from the Polygon tag and replace it with a child element named Polygon.Fill. The content of that element is the word "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>

Let's nail down some terminology. Many XAML elements refer to classes and structures and result in the creation of objects. These are known as object elements:

<Polygon ... />

Often the element contains attributes that set properties on these objects. These are known as property attributes:

Fill="Red"

It is also possible to specify a property with an alternative syntax that involves a child element. These are known as property elements:

<Polygon.Fill> Red </Polygon.Fill>

The property element is characterized by a period between the name of the element and the name of the property. Property elements have no attributes. You will never see something like this:

<!-- Wrong Syntax! --> <Polygon.Fill SomeAttribute="Whatever"> ... </Polygon.Fill>

Not even an XML namespace declaration can appear there. If you try to set an attribute on a property element in XAML Cruncher, you'll get the message "Cannot set properties on property elements," which is the message of the exception that XamlReader.Load throws when you try to do it.

The property element must contain content that is convertible to the type of the property. The Polygon.Fill property element refers to the Fill property, which is of type Brush, so the property element must have content that can be converted into a Brush:

<Polygon.Fill> Red </Polygon.Fill>

This also works:

<Polygon.Fill> #FF0000 </Polygon.Fill>

You can make the content of Polygon.Fill more explicitly a Brush with the following (rather wordier) syntax:

<Polygon.Fill> <Brush> Red </Brush> </Polygon.Fill>

Now the content of the Polygon.Fill property element is a Brush object element, the text string "Red." That content is actually convertible into an object of type SolidColorBrush, so you can write the Polygon.Fill property element like so:

<Polygon.Fill> <SolidColorBrush> Red </SolidColorBrush> </Polygon.Fill>

SolidColorBrush has a property named Color, and the ColorConverter class allows the same conversions as the Brush converter, so you can set the Color property of SolidColorBrush with a property attribute:

<Polygon.Fill> <SolidColorBrush Color="Red"> </SolidColorBrush> </Polygon.Fill>

However, you cannot now substitute Brush for SolidColorBrush because Brush does not have a property named Color.

Since SolidColorBrush has no content, you can write the tag with the empty-element syntax:

<Polygon.Fill> <SolidColorBrush Color="Red" /> </Polygon.Fill>

Or, you can break out the Color property of SolidColorBrush with the property element syntax:

<Polygon.Fill> <SolidColorBrush> <SolidColorBrush.Color> Red </SolidColorBrush.Color> </SolidColorBrush> </Polygon.Fill>

The Color property of the SolidColorBrush class is an object of type Color, so you can explicitly use an object element for the content of SolidColorBrush.Color:

<Polygon.Fill> <SolidColorBrush> <SolidColorBrush.Color> <Color> Red </Color> </SolidColorBrush.Color> </SolidColorBrush> </Polygon.Fill>

As you'll recall, Color has properties named A, R, G, and B, of type byte. You can set those properties in the Color tag with either decimal or hexadecimal syntax:

<Polygon.Fill> <SolidColorBrush> <SolidColorBrush.Color> <Color A="255" R="#FF" G="0" B="0"> </Color> </SolidColorBrush.Color> </SolidColorBrush> </Polygon.Fill>

Keep in mind that you cannot set these four properties in the SolidColorBrush.Color tag because, as the exception message says, you "cannot set properties on property elements."

Because the Color element now has no content, you can write it with the empty-element syntax:

<Polygon.Fill> <SolidColorBrush> <SolidColorBrush.Color> <Color A="255" R="#FF" G="0" B="0" /> </SolidColorBrush.Color> </SolidColorBrush> </Polygon.Fill>

Or, you could break out one or more of the attributes of Color:

<Polygon.Fill> <SolidColorBrush> <SolidColorBrush.Color> <Color A="255" G="0" B="0"> <Color.R> #FF </Color.R> </Color> </SolidColorBrush.Color> </SolidColorBrush> </Polygon.Fill>

The type of the R property in Color is Byte, a structure defined in the System namespace, and it's even possible to put a Byte element into the XAML to make the data type of R more explicit. However, the System namespace is not among the CLR namespaces associated with the two XML namespaces at the top of the XAML file. To refer to the Byte structure in a XAML file, you need another XML namespace declaration. Let's associate the System namespace with a prefix of s:

xmlns:s="clr-namespace:System;assembly=mscorlib"

Notice that the string in quotation marks begins with clr-namespace followed by a colon and a CLR namespace, just as if you were associating the prefix with a CLR namespace in your own program (as demonstrated in the UseCustomClass project in Chapter 19). Because the classes and structures in the System namespace are located in an external assembly, that information needs to follow. A semicolon follows the CLR namespace, with the word assembly, an equal sign, and the assembly name itself. Notice that a colon separates clr-namespace from the CLR namespace, but an equal sign separates assembly from the assembly name. The idea here is that the initial part of the string up through the colon is supposed to be analogous to the http: part of a conventional namespace declaration.

That declaration needs to go in the Byte element itself or a parent of the Byte element. Let's put it in the Color element (for reasons that will become apparent):

<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> </SolidColorBrush.Color> </SolidColorBrush> </Polygon.Fill>

You can go to the extreme by breaking out all four properties of Color.

<Polygon.Fill> <SolidColorBrush> <SolidColorBrush.Color> <Color xmlns:s="clr-namespace:System;assembly=mscorlib"> <Color.A> <s:Byte> 255 </s:Byte> </Color.A> <Color.R> <s:Byte> 255 </s:Byte> </Color.R> <Color.G> <s:Byte> 0 </s:Byte> </Color.G> <Color.B> <s:Byte> 0 </s:Byte> </Color.B> </Color> </SolidColorBrush.Color> </SolidColorBrush> </Polygon.Fill>

This wouldn't have worked if the new namespace declaration appeared in the first Byte element. A namespace declaration applies to the element in which it appears and all nested elements.

I hope that's as far as you want to go, because we've reached the end of the line in making XAML much more verbose than it needs to be. What this syntax demonstrates, however, is an approach that is suitable for defining a gradient brush as the Fill property.

A LinearGradientBrush has two properties named StartPoint and EndPoint. By default, these properties are in a coordinate system relative to the object they're coloring. A third crucial property is named GradientStops of type GradientStopCollection, which is a collection of GradientStop objects that indicate the colors.

The Polygon.Fill property element must have content of type Brush. An object element of type LinearGradientBrush satisfies that criterion:

<Polygon.Fill> <LinearGradientBrush ...> ... </LinearGradientBrush> </Polygon.Fill>

The StartPoint and EndPoint properties are simple enough to be defined as attribute properties in the LinearGradientBrush start tag:

<Polygon.Fill> <LinearGradientBrush StartPoint="0 0" EndPoint="1 0"> ... </LinearGradientBrush> </Polygon.Fill>

However, the GradientStops property must become a property element:

<Polygon.Fill> <LinearGradientBrush StartPoint="0 0" EndPoint="1 0"> <LinearGradientBrush.GradientStops> ... </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Polygon.Fill>

The GradientStops property is of type GradientStopCollection, so we can put in an object element for that class:

<Polygon.Fill> <LinearGradientBrush StartPoint="0 0" EndPoint="1 0"> <LinearGradientBrush.GradientStops> <GradientStopCollection> ... </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Polygon.Fill>

The GradientStopCollection class implements the IList interface, and that's sufficient to allow its members to be written as simple children. Notice that each GradientStop element is most conveniently written with the empty-element syntax:

<Polygon.Fill> <LinearGradientBrush StartPoint="0 0" EndPoint="1 0"> <GradientStopCollection> <GradientStop Offset="0" Color="Red" /> <GradientStop Offset="0.5" Color="Green" /> <GradientStop Offset="1" Color="Blue" /> </GradientStopCollection> </LinearGradientBrush> </Polygon.Fill>

This can actually be written a little simpler. It is not necessary for the LinearGradientBrush. GradientStops property element or the GradientStopCollection object element to be explicitly included. They can be removed:

<Polygon.Fill> <LinearGradientBrush StartPoint="0 0" EndPoint="1 0"> <GradientStop Offset="0" Color="Red" /> <GradientStop Offset="0.5" Color="Green" /> <GradientStop Offset="1" Color="Blue" /> </LinearGradientBrush> </Polygon.Fill>

And that's it. The Polygon.Fill property is an object of type LinearGradientBrush. LinearGradientBrush has properties StartPoint, EndPoint, and GradientStops. The LinearGradientBrush has a collection of three GradientStop objects. GradientStop has properties Offset and Color.

Here's a star with a RadialGradientBrush:

RadialGradientStar.cs

[View full width]<!-- === ================================================== RadialGradientStar.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" Stroke="Blue" StrokeThickness="5"> <Polygon.Fill> <RadialGradientBrush> <RadialGradientBrush.GradientStops> <GradientStop Offset="0" Color="Blue" /> <GradientStop Offset="1" Color="Red" /> </RadialGradientBrush.GradientStops> </RadialGradientBrush> </Polygon.Fill> </Polygon>


Let's go back to the Button. This XAML file shows three properties of Button set as attributes, including the Content property:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Foreground="LightSeaGreen" FontSize="24 pt" Content="Hello, XAML!"> </Button>

You can make the Foreground attribute a property element like this:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" FontSize="24 pt" Content="Hello, XAML!"> <Button.Foreground> LightSeaGreen </Button.Foreground> </Button>

Or you can make the FontSize attribute a property element like this:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Foreground="LightSeaGreen" Content="Hello, XAML!"> <Button.FontSize> 24 pt </Button.FontSize> </Button>

Or both Foreground and FontSize can be property elements:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Content="Hello, XAML!"> <Button.Foreground> LightSeaGreen </Button.Foreground> <Button.FontSize> 24 pt </Button.FontSize> </Button>

It's also possible to make the Content property a property element:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Foreground="LightSeaGreen" FontSize="24 pt"> <Button.Content> Hello, XAML! </Button.Content> </Button>

In this case, however, the Button.Content tags aren't required:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Foreground="LightSeaGreen" FontSize="24 pt"> Hello, XAML! </Button>

And, in fact, you can mix that content with property elements. I've inserted a couple of blank lines in this XAML file just to make it more readable:

<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Button.Foreground> LightSeaGreen </Button.Foreground> Hello, XAML! <Button.FontSize> 24 pt </Button.FontSize> </Button>

For every property of Button, you either treat the property as an attribute that you set in the Button start tag, or you use a property element where the value is a child of the elementexcept for the Content property. The Content property is special because you can simply treat the value of the Content property as a child of the Button element without using property elements.

So what makes Content so special?

Every class that you can use with XAML potentially has one property that has been identified specifically as a content property. For Button, the content property is Content. A property is identified as the content property in the definition of the class with the ContentPropertyAttribute (defined in the System.Windows.Serialization namespace). The definition of the Button class in the PresentationFramework.dll source code possibly looks something like this:

[ContentProperty("Content")] public class Button: ButtonBase { ... }

Or, Button simply inherits the setting of the ContentProperty attribute from ContentControl.

The StackPanel, on the other hand, is defined with a ContentProperty attribute that looks like this:

[ContentProperty("Children")] public class StackPanel: Panel { ... }

That ContentProperty attribute makes it possible to include the children of StackPanel as children of the StackPanel element:

<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Button HorizontalAlignment="Center"> Button Number One </Button> <TextBlock HorizontalAlignment="Center"> TextBlock in the middle </TextBlock> <Button HorizontalAlignment="Center"> Button Number One </Button> </StackPanel>

The content property of LinearGradientBrush and radialGradientBrush is GradientStops.

The content property of the TextBlock class is the Inlines collection, a collection of Inline objects. The Run class descends from Inline, and the content property of Run is Text, which is an object of type string. All of this combines to give you much freedom in defining the TextBlock content:

<TextBlock> This is <Italic>italic</Italic> text and this is <Bold>bold</Bold> text </TextBlock>

The content properties of the Italic and Bold classes are both also Inline. This piece of XAML can alternatively be written by explicitly referencing the Text property that Italic and Bold both inherit from Span:

<TextBlock> This is <Italic Text="italic" /> text and this is <Bold Text="bold" /> text </TextBlock>

Because the content property is important when writing XAML, you may be interested in getting a list of all the classes for which the ContentProperty attribute is defined and the content properties themselves. Here's a console program that provides this information.

DumpContentPropertyAttributes.cs

[View full width]//--------- ----------------------------------------------------- // DumpContentPropertyAttributes.cs (c) 2006 by Charles Petzold //------------------------------------------------ -------------- using System; using System.Collections.Generic; using System.Reflection; using System.Windows; using System.Windows.Markup; using System.Windows.Navigation; public class DumpContentPropertyAttributes { [STAThread] public static void Main() { // Make sure PresentationCore and PresentationFramework are loaded. UIElement dummy1 = new UIElement(); FrameworkElement dummy2 = new FrameworkElement(); // SortedList to store class and content property. SortedList<string, string> listClass = new SortedList<string, string>(); // Formatting string. string strFormat = "{0,-35}{1}"; // Loop through the loaded assemblies. foreach (AssemblyName asmblyname in Assembly.GetExecutingAssembly ().GetReferencedAssemblies()) { // Loop through the types. foreach (Type type in Assembly.Load (asmblyname).GetTypes()) { // Loop through the custom attributes. // (Set argument to 'false' for non-inherited only!) foreach (object obj in type .GetCustomAttributes( typeof (ContentPropertyAttribute), true)) { // Add to list if ContentPropertyAttribute. if (type.IsPublic && obj as ContentPropertyAttribute != null) listClass.Add(type.Name, (obj as ContentPropertyAttribute).Name); } } } // Display the results. Console.WriteLine(strFormat, "Class", "Content Property"); Console.WriteLine(strFormat, "-----", "----------------"); foreach (string strClass in listClass.Keys) Console.WriteLine(strFormat, strClass, listClass[strClass]); } }


Some elementsmostly notably DockPanel and Gridhave attached properties that you use in C# code with syntax like this:

DockPanel.SetDock(btn, Dock.Top);

This code indicates that you want the btn control to be docked at the top of a DockPanel. The call has no effect if btn is not actually a child of a DockPanel. As you discovered in Chapter 8, the call to the static SetDock method is equivalent to:

btn.SetValue(DockPanel.DockProperty, Dock.Top);

In XAML, you use a syntax like this:

<Button DockPanel.Dock="Top" ... />

Here's a little stand-alone XAML file somewhat reminiscent of the DockAroundTheBlock program from Chapter 6 but not nearly as excessive.

AttachedPropertiesDemo.xaml

[View full width]<!-- === ====================================================== AttachedPropertiesDemo.xaml (c) 2006 by Charles Petzold ============================================= ============ --> <DockPanel xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml"> <Button Content="Button No. 1" DockPanel .Dock="Left" /> <Button Content="Button No. 2" DockPanel .Dock="Top" /> <Button Content="Button No. 3" DockPanel .Dock="Right" /> <Button Content="Button No. 4" DockPanel .Dock="Bottom" /> <Button Content="Button No. 5" /> </DockPanel>


The Grid works particularly well in XAML because the row and column definitions aren't nearly as verbose as their C# equivalents. Each RowDefinition and ColumnDefinition can occupy a single line within Grid.RowDefinitions and Grid.ColumnDefinitions property elements. The following is another stand-alone XAML file.

SimpleGrid.xaml

[View full width]<!-- ============================================= SimpleGrid.xaml (c) 2006 by Charles Petzold ============================================= --> <Grid xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml"> <Grid.RowDefinitions> <RowDefinition Height="100" /> <RowDefinition Height="Auto" /> <RowDefinition Height="100*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="33*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="67*" /> </Grid.ColumnDefinitions> <Button Content="Button No. 1" Grid.Row="0" Grid.Column="0" /> <Button Content="Button No. 2" Grid.Row="1" Grid.Column="0" /> <Button Content="Button No. 3" Grid.Row="2" Grid.Column="0" /> <GridSplitter HorizontalAlignment="Center" Width="6" Grid.Row="0" Grid.Column="1" Grid.RowSpan="3" /> <Button Content="Button No. 4" Grid.Row="0" Grid.Column="2" /> <Button Content="Button No. 5" Grid.Row="1" Grid.Column="2" /> <Button Content="Button No. 6" Grid.Row="2" Grid.Column="2" /> </Grid>


If you're satisfied with the default Height and Width of "1*" (to use the XAML syntax) you can even write RowDefinition and ColumnDefinition elements like this:

<RowDefinition />

Attached properties aren't the only attributes that can contain periods. You can also define an element's properties with attributes that contain a class name preceding the property name. The class name can be the same as the element in which the attribute appears, or an ancestor class, or a class that is also an owner of the same dependency property. For example, these are all valid attributes for a Button element:

Button.Foreground="Blue" TextBlock.FontSize="24pt" FrameworkElement.HorizontalAlignment="Center" ButtonBase.VerticalAlignment="Center" UIElement.Opacity="0.5"

In some cases, you can use an attribute containing a class name and a property in an element in which that property is not defined. Here's an example.

PropertyInheritance.xaml

[View full width]<!-- === =================================================== PropertyInheritance.xaml (c) 2006 by Charles Petzold ============================================= ========= --> <StackPanel xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" HorizontalAlignment="Center" TextBlock.FontSize="16pt" TextBlock.Foreground="Blue" > <TextBlock> Just a TextBlock </TextBlock> <Button> Just a Button </Button> </StackPanel>


This XAML file sets the FontSize and Foreground properties in the StackPanel element, a class that doesn't have these properties, so the properties must be prefaced by a class that does define the properties. These attributes result in both the TextBlock and the Button getting a font size of 16 points, but a mysterious quirk results in only the TextBlock getting a blue foreground.

You can also use an attribute with a class and event name to set a handler for a routed event. The handler affects all child elements. The RoutedEventDemo project contains two files, RoutedEventDemo.xaml and RoutedEventDemo.cs. The XAML file contains an attribute in the ContextMenu element of MenuItem.Click. This handler applies to the MenuItem elements that comprise the context menu of a TextBlock.

RoutedEventDemo.xaml

[View full width]<!-- === =============================================== RoutedEventDemo.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.RoutedEventDemo .RoutedEventDemo" Title="Routed Event Demo"> <TextBlock Name="txtblk" FontSize="24pt" HorizontalAlignment="Center" VerticalAlignment="Center" ToolTip="Right click to display context menu"> TextBlock with Context Menu <TextBlock.ContextMenu> <ContextMenu MenuItem .Click="MenuItemOnClick"> <ContextMenu> <MenuItem Header="Red" /> <MenuItem Header="Orange" /> <MenuItem Header="Yellow" /> <MenuItem Header="Greem" /> <MenuItem Header="Blue" /> <MenuItem Header="Indigo" /> <MenuItem Header="Violet" /> </ContextMenu> </TextBlock.ContextMenu> </TextBlock> </Window>


The C# file contains a handler for that event. The MenuItem object that triggered the event is the Source property of the RoutedEventArgs object. Notice that the handler uses the static ColorConverter.ConvertFromString method to convert the MenuItem text to a Color object.

RoutedEventDemo.cs

[View full width]//-------------------------------------------------- // RoutedEventDemo.cs (c) 2006 by Charles Petzold //-------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.RoutedEventDemo { public partial class RoutedEventDemo : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new RoutedEventDemo()); } public RoutedEventDemo() { InitializeComponent(); } void MenuItemOnClick(object sender, RoutedEventArgs args) { string str = (args.Source as MenuItem) .Header as string; Color clr = (Color)ColorConverter .ConvertFromString(str); txtblk.Foreground = new SolidColorBrush(clr); } } }


You've now seen several cases where an element or an attribute can contain a period. Here's a summary:

If the element name doesn't contain a period, it's always the name of a class or a structure:

<Button ... />

If the element name contains a period, the element name consists of the name of a class or structure followed by a property in that class structure. It's a property element:

<Button.Background> ... </Button.Background>

This particular element must appear as a child of a Button element, and the start tag never has attributes. This element must contain content that is convertible to the type of the property (for example, simply the text string "Red" or "#FF0000") or a child element of the same type as the property (for example, Brush or any class that derives from Brush).

Attribute names usually do not contain periods:

< ... Background="Red" ... >

These attributes correspond to properties of the element in which they appear. When an attribute contains a period, one possibility is that it's an attached property:

< ... DockPanel.Dock="Left" ... >

This particular attached property usually appears in a child of a DockPanel, but that's not a requirement. If this attached property appears in an element that is not a child of a DockPanel, it is simply ignored.

An attribute with a period can also be a routed input event:

< ... MenuItem.Click="MenuItemOnClick" ... >

It makes most sense for this attribute to appear not in a MenuItem element (because the attribute name could simply be Click in that case) but in an element of which multiple MenuItem elements are children. It's also possible for periods to appear in property definitions that are intended to be inherited by children:

< ... TextBlock.FontSize="24pt" ... >

I think you'll agree that in many cases XAML is more concise than equivalent C# code, and it better represents hierarchical structures such as those that arise in laying out a window with panels and controls. But markup in general is much more limited than procedural languages, mostly because there's no concept of flow control. Even the simple sharing of variables seems unlikely in XAML. As you'll begin to see in the next chapter, however, XAML has a number of features that compensate for these deficiencies.


Previous Page Next Page


Sections