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