What is the easiest way to set spacing between items in StackPanel?

29 במאי 2011

5 תגובות

What is the easiest way to set spacing between items in StackPanel or any other Panel?

Seems like an easy question, but most of the time it requires just too much xaml work.

If we’re just putting items of the same type, we can use “anonymous” styles, like so:

   1: <StackPanel>

   2:     <StackPanel.Resources>

   3:         <Style TargetType="Button">

   4:             <Setter Property="Margin" Value="5" />

   5:         </Style>

   6:     </StackPanel.Resources>

   7:     <Button Content="Hello" />

   8:     <Button Content="Hello" />

   9:     <Button Content="Hello" />

  10: </StackPanel>

Due note, that even for this basic situation we have to resort to a LOT of code. Many times, XAML is just too damn verbose. I don’t enjoy writing 5 (!) lines of code for something this trivial, especially compared to how elegant HTML & CSS can be in this situation.

To make things worse, this is not even the worst case… what if I have different controls inside the StackPanel?

The first intuition of most programmers is to target a base class that everybody shares, like so:

   1: <StackPanel>

   2:     <StackPanel.Resources>

   3:         <Style TargetType="Control">

   4:             <Setter Property="Margin" Value="5" />

   5:         </Style>

   6:     </StackPanel.Resources>

   7:     <TextBox Text="Hello" />

   8:     <Button Content="Hello" />

   9:     <Button Content="Hello" />

  10: </StackPanel>

Although this intuition makes sense, this simply doesn’t work (because of DependecyProperties Value Resolution. Default value of a class has higher precedence than a Style).

Seemingly, we’re stuck. And when we’re stuck in WPF there’s almost always a cool solution – Attached Behaviors!

MarginSetter Attached Property

An Attached Property (or Behavior) can come very handy in this situation. All we need to do is to create an attached property that can be attached to anything that inherits from Panel, and that iterate over its children and set a margin for them:

   1: public class MarginSetter

   2: {

   3:     public static Thickness GetMargin(DependencyObject obj)

   4:     {

   5:         return (Thickness)obj.GetValue(MarginProperty);

   6:     }

   7:  

   8:     public static void SetMargin(DependencyObject obj, Thickness value)

   9:     {

  10:         obj.SetValue(MarginProperty, value);

  11:     }

  12:  

  13:     // Using a DependencyProperty as the backing store for Margin.  This enables animation, styling, binding, etc...

  14:     public static readonly DependencyProperty MarginProperty =

  15:         DependencyProperty.RegisterAttached("Margin", typeof(Thickness), typeof(MarginSetter), new UIPropertyMetadata(new Thickness(), MarginChangedCallback));

  16:  

  17:     public static void MarginChangedCallback(object sender, DependencyPropertyChangedEventArgs e)

  18:     {

  19:         // Make sure this is put on a panel

  20:         var panel = sender as Panel;

  21:  

  22:         if (panel == null) return;

  23:  

  24:  

  25:         panel.Loaded += new RoutedEventHandler(panel_Loaded);

  26:  

  27:     }

  28:  

  29:     static void panel_Loaded(object sender, RoutedEventArgs e)

  30:     {            

  31:         var panel = sender as Panel;

  32:  

  33:         // Go over the children and set margin for them:

  34:         foreach (var child in panel.Children)

  35:         {

  36:             var fe = child as FrameworkElement;

  37:  

  38:             if (fe == null) continue;

  39:  

  40:             fe.Margin = MarginSetter.GetMargin(panel);

  41:         }

  42:     }

  43:  

  44:  

  45: }

 

The beauty of Attached Properties is once you write an added behavior in this fashion, it’s fully encapsulated, and you can use it extremely easy. To use MarginSetter, you would use the following syntax: local:MarginSetter.Margin="5"

   1: <StackPanel local:MarginSetter.Margin="5">

   2:     <TextBox Text="hello" />

   3:     <Button Content="hello" />

   4:     <Button Content="hello" />

   5: </StackPanel>

The result is just what we wanted – every child of the StackPanel now has a Margin of 5 !

tmp39A9

הוסף תגובה
facebook linkedin twitter email

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

5 תגובות

  1. Malone1 ביוני 2011 ב 10:45

    Great post. Exactly what I was searching for.

    הגב
  2. Malone1 ביוני 2011 ב 10:54

    Suggestion:

    panel.Loaded -= Panel_Loaded;
    panel.Loaded += Panel_Loaded;

    To prevent multiple registration due to value change in code behind or with binding.

    הגב
  3. eladkatz2 ביוני 2011 ב 3:45

    Thanks Malone 🙂

    As for the memory issues – you're basically right, though i'm not seeing how could that be bad here – can u care to explain?

    הגב
  4. Olga23 באוגוסט 2012 ב 4:47

    Hello @021cb7d4340fe225c9db8ff3ec565b7e,Try it out. It will work for you. "Run" itierhns from "Inline", which itierhns from "TextElement". TextElement has it's base class as "DependencyObject". Hence, you will be able to data bind in Run element. Try with a sample code and let me know.Cheers,Kunal

    הגב
  5. Kim25 באוגוסט 2012 ב 8:02

    Hey! Just wanted to say a quick thkans for your blog… I've stumbled across a few great tips of yours whilst googling for solutions to some WP7 development problems.Cheers!

    הגב