Search code examples
xamltreeviewhierarchicaldatatemplate

Hierarchical Template Formatting Problems / Questions


I have a couple of questions about hierarchical template formatting for a TreeView. This image will illustrate:

enter image description here

  • I want to remove the extra space between the top of the type and the border.
  • I want to center the icon between the two lines of type
  • I want to add a thousands comma. I've tried this but there is a problem with adding a comma with a bound data.

Here is the XAML code for the third level:

<HierarchicalDataTemplate 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    ItemsSource="{Binding XPath=Unit}"
    >
    <Grid Height="42" Width="auto" >
        <Grid Height="41" HorizontalAlignment="Left" Margin="0,0,0,0" Name="grid1" VerticalAlignment="Top" Width="auto">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto" />
                <ColumnDefinition Width="auto" />
                <ColumnDefinition Width="auto" />
                <ColumnDefinition Width="100" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="auto" />
                <RowDefinition Height="auto" />
            </Grid.RowDefinitions>
            <Image Source= "{Binding XPath=UnitIcon}" Grid.Column="1" Grid.RowSpan="2" VerticalAlignment="Center"  HorizontalAlignment="Left" Stretch="None" OpacityMask="White"></Image>
            <Label Content="{Binding XPath=UnitName}" Height="54" HorizontalAlignment="Left" Name="label4" VerticalAlignment="Top" FontFamily="Smythe" FontSize="18"  Margin="0,0,0,0"  Grid.RowSpan="3" Grid.Column="2" Grid.ColumnSpan="3"/>
            <Label Content="Strength:"  FontFamily="Amaltea WF" FontSize="12" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Center"  Grid.Column="2" Grid.Row="2"/>
            <TextBlock  Text="{Binding XPath=UnitStrength, ConverterParameter=N0}" Margin="0,0,0,0" FontFamily="BauderieScriptSSK Bold" FontSize="18"   HorizontalAlignment="Left" VerticalAlignment="Center" Grid.Column="3" Grid.Row="2"/>
                
        </Grid>
         <Line X1='0'
              X2='200'
              Y1='0'
              Y2='0'
              Stroke="Gray"
              StrokeThickness='1' />
    </Grid>
</HierarchicalDataTemplate>

Solution

    1. Take the fixed height off the UnitName label. You've got grid cells, you don't want fixed heights. Part of that gap may be the line height from your font. Temporarily set Background="LightSkyBlue" on the label to see how much space the label itself is actually taking up.

    2. Looks like VerticalAlignment="Center" on the image isn't having the desired effect because you've put conflicting fixed heights on everything. Your grid1 is fixed at 41 units high, but the unit name within it is 54 units high. The layout engine is doing its best to comply with the contradictory orders you're giving it.

      Delete every fixed height in your XAML. Every one, no exceptions. Let things size themselves. If you absolutely must impose a fixed height on a control, consider putting its contents in a ViewBox, so the contents can dynamically size themselves without overflowing the container. Or not; that can look weird. But first get your relative layout working, and then start working on cramming it down into whatever limited space you've got for it.

      When you're having trouble with XAML layout, the naive impulse is to add stuff. And worst of all, to add random stuff -- "I don't know what this property means or what its value means, but maybe if I add it on this control, it'll fix what's wrong with the other one!" -- at best, the stuff you add will be harmless.

      Don't do that. Remove stuff instead, then build back up. Add one thing at a time and see what it does. And add nothing without first reading the documentation on it. Adding six random properties from Intellisense seems to take less time than looking up one property on MSDN, but that turns out not to be the case in practice, because the first approach is always guaranteed to be a total waste of time. It's like driving by closing your eyes and trying to steer by the feel of the obstacles you crash into.

    3. You're assigning the right format string to the wrong property. Try this:

      <TextBlock Text="{Binding XPath=UnitStrength, StringFormat=N0}"
      

      Except whoops LOL ha ha that doesn't work with Binding.XPath, so I'm talking nonsense. And neither does this:

      <Label Content="{Binding XPath=UnitStrength}" ContentStringFormat="N0" />
      

      I suspect they're failing because you're giving them a string rather than an integer, but that's just a guess.

      But this works.

      public class IntToStringConverter : IValueConverter
      {
          public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
          {
              int n = 0;
      
              if (Int32.TryParse((string)value, out n))
              {
                  value = n.ToString((String)parameter);
              }
              return value;
          }
      
          public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
          {
              throw new NotImplementedException();
          }
      }
      

      XAML. YOUR_NAMESPACE_HERE is the C# namespace where you defined the IntToStringConverter class. That doesn't necessarily have to be right there; it could be on the parent tag or any containing tag in this XAML file, including the root tag (Window or UserControl or ResourceDictionary). Putting it here makes the example more self-contained.

      <HierarchicalDataTemplate
          xmlns:local="clr-namespace:YOUR_NAMESPACE_HERE"
          >
          <HierarchicalDataTemplate.Resources>
              <local:IntToStringConverter
                  x:Key="IntToString"
                  />
          </HierarchicalDataTemplate.Resources>
      
          <!-- blah blah -->
      
          <TextBlock 
              Text="{Binding XPath=UnitStrength, Converter={StaticResource IntToString}, ConverterParameter=N0}" 
              />
      

    Update

    <Window.Resources>
        <!-- stuff -->
    
        <HierarchicalDataTemplate 
            x:Key="UnitTemplate"
            ItemsSource="{Binding XPath=Unit}"
            >
            <Grid Width="auto">
                <!-- stuff -->
            </Grid>
        </HierarchicalDataTemplate>
    
        <!-- stuff -->
    </Window.Resources>
    

    And for the TreeView:

        <TreeView
            ...
            ItemTemplate="{StaticResource UnitTemplate}"
            ...
            />
    

    But this works too, if a template is going to be used in only one place:

    <TreeView
        ...
        >
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate 
                ItemsSource="{Binding XPath=Unit}"
                >
                <Grid Width="auto">
                    <!-- stuff -->
                </Grid>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
    

    Another Upate

    Or finally, if you want to put all your data templates in a file of their own, you want to create a resource dictionary:

    <Window.Resources>
        <!-- If you're doing the merged thing, you have to explicitly have the 
             ResourceDictionary tag here. 
        -->
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="DataTemplates.xaml" />
            </ResourceDictionary.MergedDictionaries>
    
            <!-- other resources maybe -->
        </ResourceDictionary>
    </Window.Resources>
    

    DataTemplate.xaml

    <ResourceDictionary 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ScenarioEditor"
        >
        <local:IntToStringConverter
            x:Key="IntToString"
            />
    
        <HierarchicalDataTemplate 
            x:Key="UnitTemplate"
            ItemsSource="{Binding XPath=Unit}"
            >
            <Grid Width="auto">
                <!-- stuff -->
            </Grid>
        </HierarchicalDataTemplate>
    
        <HierarchicalDataTemplate 
            x:Key="SomeOtherTemplate"
            >
            <Grid Width="auto">
                <!-- different stuff -->
            </Grid>
        </HierarchicalDataTemplate>
    </ResourceDictionary>        
    

    Yet Another Update

    So the tree we're looking at has multiple levels, with a different template for each level. There are two ways to do this, at least: If we had a tree of .NET classes with different child types, we could define "implicit templates" in a resource dictionary. They'd have a DataType attribute rather than x:Key, with the result that (for example) the template with DataType="{x:Type local:HQ}" would automatically be used to display any class instance of that type.

    But you've got XML so that's not going to work. What you can do instead is give each HierarchicalDataTemplate its own ItemTemplate. For clarity, the following example omits ItemsSource and much else -- it only illustrates how we set up those parent/child template relationships.

    <HierarchicalDataTemplate
        x:Key="UnitTemplate"
        >
        <Grid>
            <!-- Unit stuff -->
        </Grid>
    </HierarchicalDataTemplate>
    
    <HierarchicalDataTemplate 
        x:Key="CommanderTemplate"
        ItemTemplate="{StaticResource UnitTemplate}"
        >
        <Grid>
            <!-- Commander stuff -->
        </Grid>
    </HierarchicalDataTemplate>
    
    <HierarchicalDataTemplate 
        x:Key="HQTemplate"
        ItemTemplate="{StaticResource CommanderTemplate}"
        >
        <Grid>
            <!-- HQ stuff -->
        </Grid>
    </HierarchicalDataTemplate>
    

    HQTemplate will be the treeview's ItemTemplate

    <TreeView 
        ...
        ItemTemplate="{StaticResource HQTemplate}"