次の方法で共有


WPF, Silverlight and C# 3.0 object initializers

XAML is definitely the way to go whenever possible when you're writing WPF and Silverlight apps, due to its amenability to tooling, analyzability, side-effect-free-ness, etc.  However, there are occasionally (or often, depending on what you're doing) times when you need to construct WPF and Silverlight objects via code, and pure XAML expression doesn't do the trick.  A common example is when you're in a programmatic loop, and you need to generate an object on each iteration of the loop; or when you have something dynamically varying at runtime and you create based on that.  Some of those situations may still work with XAML and databinding, but there are many times where you just want to back off to writing it straight in code.

Since XAML is really an object-initialization-and-property-setting language, consider how we write the following XAML in code:

      <Canvas>

            <Rectangle StrokeThickness="30" RadiusX="97.5" RadiusY="97.5" Width="396" Height="312" >

                  <Rectangle.Fill>

                        <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">

                              <GradientStop Color="White" Offset="0"/>

                              <GradientStop Color="Blue" Offset="0.5"/>

                              <GradientStop Color="Black" Offset="1"/>

                        </LinearGradientBrush>

                  </Rectangle.Fill>

                  <Rectangle.Stroke>

                        <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">

                              <GradientStop Color="Black" Offset="0"/>

<GradientStop Color="Red" Offset="0.5"/>

                  <GradientStop Color="White" Offset="1"/>

                        </LinearGradientBrush>

                  </Rectangle.Stroke>

            </Rectangle>

            <TextBlock Width="172" Height="80" Canvas.Left="109" Canvas.Top="109" TextWrapping="Wrap"

            FontFamily="Baskerville Old Face" FontSize="72" Foreground="Orange">Hello

            </TextBlock>

      </Canvas>

The canonical means for writing something like this in code is to go from the inside out, creating objects and store them in local variables, and then assigning them into the containing objects.  So, the above would be constructed like this in C#:

        public UIElement CreateMyObject(double cornerRadius, double strokeThickness,

                                        string textString)

        {

            LinearGradientBrush fillBrush = new LinearGradientBrush();

            fillBrush.StartPoint = new Point(0, 0.5);

            fillBrush.EndPoint = new Point(1, 0.5);

            fillBrush.GradientStops.Add(new GradientStop(Colors.White, 0.0));

            fillBrush.GradientStops.Add(new GradientStop(Colors.Blue, 0.5));

            fillBrush.GradientStops.Add(new GradientStop(Colors.Black, 1.0));

            LinearGradientBrush strokeBrush = new LinearGradientBrush();

            strokeBrush.StartPoint = new Point(0, 0.5);

            strokeBrush.EndPoint = new Point(1, 0.5);

            strokeBrush.GradientStops.Add(new GradientStop(Colors.Black, 0.0));

            strokeBrush.GradientStops.Add(new GradientStop(Colors.Red, 0.5));

            strokeBrush.GradientStops.Add(new GradientStop(Colors.White, 1.0));

            Rectangle r = new Rectangle();

            r.Height = 312;

            r.Width = 396;

            r.Fill = fillBrush;

            r.Stroke = strokeBrush;

            r.StrokeThickness = strokeThickness;

            r.RadiusX = cornerRadius;

            r.RadiusY = cornerRadius;

            TextBlock t = new TextBlock();

            t.TextWrapping = TextWrapping.Wrap;

            t.FontFamily = new FontFamily("Baskerville Old Face");

            t.FontSize = 72;

            t.Foreground = Brushes.Orange;

            t.Text = textString;

            t.SetValue(Canvas.LeftProperty, 109.0);

            t.SetValue(Canvas.TopProperty, 109.0);

            Canvas c = new Canvas();

            c.Children.Add(r);

            c.Children.Add(t);

            return c;

        }

This certainly works, but it has the downside of taking something that's fundamentally declarative, and adding assignment to it solely because we need to refer to the objects we just created as we assign its properties.  This approach also encourages the inside-out construction of objects, making it harder to see the forest through the trees.

Enter C# 3.0 (coming in the "Orcas" release), with its "object initializer" feature.  Object initializers are particularly good for the sorts of objects that get built up in WPF and Silverlight -- namely objects that are created and then have properties assigned.  Not coincidentally, this is precisely the pattern used for XAML-exposed objects as well.

Here's a canonical example of construction of a Point without and with object initializers (not using any parameterized constructors):

Without:

        Point pt = new Point();

        pt.X = 3.0;

        pt.Y = 4.0;

With:

        Point pt = new Point() { X = 3.0, Y = 4.0 };

Now we can take our C# and convert it to use object initializers, resulting in this:

        public UIElement CreateMyObject(double cornerRadius, double strokeThickness,

                                        string textString)

        {

            return new Canvas()

               {

                   Children =

                   {

                       new Rectangle()

    {

                           Height = 312,

                           Width = 396,

                           Fill = new LinearGradientBrush()

                                  {

                                      StartPoint = new Point(0,0.5),

                                      EndPoint = new Point(1, 0.5),

                                      GradientStops =

                                      {

                                          new GradientStop(Colors.White, 0),

  new GradientStop(Colors.Blue, 0.5),

                                          new GradientStop(Colors.Black, 1),

                                      }

                                  },

                           Stroke = new LinearGradientBrush()

                                  {

                                      StartPoint = new Point(0,0.5),

                                      EndPoint = new Point(1, 0.5),

                     GradientStops =

                                      {

                                          new GradientStop(Colors.Black, 0),

                                          new GradientStop(Colors.Red, 0.5),

                            new GradientStop(Colors.White, 1),

                                      }

                                  },

                           StrokeThickness = strokeThickness,

                           RadiusX = cornerRadius,

                RadiusY = cornerRadius

                       },

                       new TextBlock()

                       {

                           TextWrapping = TextWrapping.Wrap,

                           FontFamily = new FontFamily("Baskerville Old Face"),

                           FontSize = 72,

                           Foreground = Brushes.Orange,

                           Text = textString,

                       }

                   }

               };

        }

Here, objects can be created outside-in, there's no possibility of creating unwanted "side-effects" here by needing to deal with temporary local variables.  Finally, C# Intellisense in Visual Studio helps out even more in object initializer expressions -- by only displaying properties that can be set (no methods), and by not displaying properties that have already been set in the expression.  So for instance when I'm typing FontSize in the above, Intellisense won't prompt with FontFamily, since this has already been set the line before.

There is one major problem with the object-initializer version I show above: it doesn't deal with the attached properties (Canvas.Top and Canvas.Left) that both the XAML and the first C# example have.  C# doesn't have direct language support for attached properties, so this is a situation where we do need to have a compromise approach -- using object initializers to create as large a chunk as possible, then setting attached properties imperatively, then using the results in another object initializer expression.  The following takes that approach as it creates a TextBlock with object initialization syntax, sets some attached properties, and then uses the result in another object initializer expression.

        public UIElement CreateMyObject(double cornerRadius, double strokeThickness,

                                        string textString)

        {

            TextBlock textBlock = new TextBlock()

                       {

                           TextWrapping = TextWrapping.Wrap,

                           FontFamily = new FontFamily("Baskerville Old Face"),

                           FontSize = 72,

                           Foreground = Brushes.Orange,

                           Text = textString,

                       };

    textBlock.SetValue(Canvas.LeftProperty, 109.0);

            textBlock.SetValue(Canvas.TopProperty, 109.0);

            return new Canvas()

               {

                   Children =

                   {

                       new Rectangle()

                       {

                           Height = 312,

                           Width = 396,

                           Fill = new LinearGradientBrush()

                                  {

                                      StartPoint = new Point(0,0.5),

                                      EndPoint = new Point(1, 0.5),

                                      GradientStops =

                                      {

                                          new GradientStop(Colors.White, 0),

  new GradientStop(Colors.Blue, 0.5),

                                          new GradientStop(Colors.Black, 1),

                                      }

                                  },

                         Stroke = new LinearGradientBrush()

                                  {

                                      StartPoint = new Point(0,0.5),

                                      EndPoint = new Point(1, 0.5),

                                      GradientStops =

                                      {

                                          new GradientStop(Colors.Black, 0),

                                          new GradientStop(Colors.Red, 0.5),

                                          new GradientStop(Colors.White, 1),

                                      }

                                  },

                           StrokeThickness = strokeThickness,

                           RadiusX = cornerRadius,

                           RadiusY = cornerRadius

                       },

                       textBlock

                   }

               };

All told, C# 3.0 object initializers make programmatic construction of WPF and Silverlight objects much more pleasant.

Comments

  • Anonymous
    May 19, 2007
    The XAML code is very readable. The first C# translation is also very readable. However, the C# translations using object initializer are extremely unreadable. XAML and C# are two different languages with different purposes. There is no need to try to imitate the syntax of one in another. I like the new object initializer syntax, but it is code like this that abuses it. I really hope that I do not inherit or need to maintain code as you are suggesting.

  • Anonymous
    May 21, 2007
    I disagree with "me."  I think the object initalizer version is more readable.  It makes the containment relationships and the object properties obvious.  I hate having to chase down the meaning of 10, often poorly named, method local variables to try and understand the overall structure of the composite object. If by unreadable "me" means it doesn't look like c#, he is correct.  There is a learning curve associated with having declarative constructs in your imperative program.  I have, however, used similar idioms using constructors and factory methods that have been very understandable and maintainable.

  • Anonymous
    May 22, 2007
    I know this is was supposed to be illustrative about how object initializers can be used to create object hierarchies in C# top down. But in most cases wouldn't it still be preferable to use LoadXaml to instantiate the xaml and then if you have to do extra manipulation to that tree do it on the hierarchy created?

  • Anonymous
    May 22, 2007
    Re: "me"'s comment.  This is clearly a stylistic, judgment thing.  Like "John Melville", I find the object initializer syntax more clear and more revealing of my intent. Re: "sdether"'s comment.  I agree that constructing via fixed XAML and then doing surgical modifications may be the better way to go here, depending on the situation.  For me to want to do that, the overwhelming amount of stuff being constructed would need to be static, and the dynamic stuff would need to be easily accessed programmatically.  Otherwise I'd prefer to build it up programmatically.   And of course there are in-betweens, like if I had declared the gradient brushes as resources in XAML and used them in code by accessing the resource dictionary, that would certainly have been a nice way to go as well.

  • Anonymous
    May 22, 2007
    So I've had a (very) small amount of time to play around with Orcas and Silverlight, but some of the