简体   繁体   中英

WPF binding in data template not working for custom class

Working on a ComboBox that displays a list of available tile backgrounds. This is just a simple ComboBox with an ItemSource set to a collection of MapTileBackground objects.

The MapTileBackground class is defined entirely with properties:

public partial class MapTileBackground
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public byte[] Content { get; set; }
    public Nullable<int> Color { get; set; }
    public int StrokeColor { get; set; }
    public byte StrokeThickness { get; set; }
}

which is defined in a separate library and I would prefer to not change it.

I have defined a simple shape extension to draw the background::

public class MapTileBackgroundPreview : Shape
{
    public static readonly DependencyProperty SizeProperty = DependencyProperty.Register("Size", typeof(Point), typeof(MapTileBackgroundPreview));
    public static readonly DependencyProperty TileBackgroundProperty = DependencyProperty.Register("TileBackground", typeof(MapTileBackground), typeof(MapTileBackgroundPreview));
    public MapTileBackgroundPreview()
    {
        layout = new Hex.Layout(Hex.Orientation.Flat, new Hex.Point(8, 8), new Hex.Point(4, 4));
        Size = new Point(8, 8);
        TileBackground = null;
    }

    private Hex.Layout layout;
    protected override Geometry DefiningGeometry
    {
        get
        {
            var points = layout.HexCorners(0, 0).ToArray();
            var path = new PathFigure();
            path.StartPoint = points[5].ToWin();
            for (var i = 0; i < 6; i++)
                path.Segments.Add(new LineSegment(points[i].ToWin(), true));

            var geo = new PathGeometry();
            geo.Figures.Add(path);
            return geo;
        }
    }
    public Point Size
    {
        get
        {
            return (Point)GetValue(SizeProperty);
        }
        set
        {
            SetValue(SizeProperty, value);
            layout.Size = value.ToHex();
            layout.Origin = new Hex.Point(layout.Size.X / 2, layout.Size.Y / 2);
        }
    }

    public MapTileBackground TileBackground
    {
        get
        {
            return (MapTileBackground)GetValue(TileBackgroundProperty);
        }
        set
        {
            SetValue(TileBackgroundProperty, value);

            if (value == null)
            {
                Fill = Brushes.Transparent;
                Stroke = Brushes.Black;
                StrokeThickness = 1;
            }
            else
            {
                Stroke = value.Stroke();
                StrokeThickness = value.StrokeThickness();
                Fill = value.Fill(layout.Orientation);
            }
        }
    }
}

The layout is just a conversion utility between screen pixel coordinates and a hexagonal system. DefiningGeometry just add 6 line segments of the hex. The TileBackground setter, when given a not null MapTileBackground, updates the Stroke and Fill as the background defines. I've tested this control successfully (outside the combo box data template).

And speaking of:

<DataTemplate x:Key="TileListItemRenderer">
    <Grid Width="225">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="24"/>
            <ColumnDefinition Width="75"/>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <local:MapTileBackgroundPreview Grid.Row="0" Grid.Column="0" Size="12,12" VerticalAlignment="Center" HorizontalAlignment="Center" TileBackground="{Binding /}"/>
        <Label Grid.Row="0" Grid.Column="1" Content="{Binding Name}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
        <TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Description}" HorizontalAlignment="Stretch" VerticalAlignment="Top" TextWrapping="Wrap" />
    </Grid>
</DataTemplate>

So I just create a shape, and two labels, bind the shape to the current MapTileBackground object (combo box ItemSource is a collection of MapTileBackground objects), and the labels to Name and Description.

My problem is the shape is always drawn empty (as in TileBackground is null) and the setter is never invoked. Both the Name Label and Description TextBlock behave as expected (display correct text). And during my debugging attempts, I created an id property on the preview object which in turn invokes the TileBackground Setter and bound it to the Id property (avoid current object bind), again, the TileBackgroundId setter is never invoked. I even added a new label bound to Id to see if that was working and it displays the id as expected. Here are those changes that again did not work. The TileBackgroundId or TileBackground properties are never set when opening the drop down.

    <DataTemplate x:Key="TileListItemRenderer">
        <Grid Width="225">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="24"/>
                <ColumnDefinition Width="75"/>
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <local:MapTileBackgroundPreview Grid.Row="0" Grid.Column="0" Size="12,12" VerticalAlignment="Center" HorizontalAlignment="Center" TileBackgroundId="{Binding Id}"/>
            <Label Grid.Row="0" Grid.Column="0" Content="{Binding Id}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
            <Label Grid.Row="0" Grid.Column="1" Content="{Binding Name}" HorizontalAlignment="Left" VerticalAlignment="Center" FontWeight="Bold"/>
            <TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Description}" HorizontalAlignment="Stretch" VerticalAlignment="Top" TextWrapping="Wrap" />
        </Grid>
    </DataTemplate>

    public static readonly DependencyProperty TileBackgroundIdProperty = DependencyProperty.Register("TileBackgroundId", typeof(int), typeof(MapTileBackgroundPreview));

    public int TileBackgroundId
    {
        get
        {
            return (int)GetValue(TileBackgroundIdProperty);
        }
        set
        {
            SetValue(TileBackgroundIdProperty, value);
            TileBackground = TMapTileBackgroundTool.Get(value);
        }
    }

TMapTileBackgroundTool.Get() returns the correct object based on Id.

I have also tested instances of MapTileBackgroundPreview setting TileBackgroundId outside the data template.

Any thoughts as to what is going on?

The setter of the CLR wrapper for the dependency property is not supposed to be set as the WPF binding engine calls the GetValue and SetValue methods directly:

Setters not run on Dependency Properties?

Why are .NET property wrappers bypassed at runtime when setting dependency properties in XAML?

The getter and setter of the CLR wrapper property should only call the GetValue and SetValue method respectively.

If you want to do something when the dependency property is set, you should register a callback:

public static readonly DependencyProperty TileBackgroundIdProperty = DependencyProperty.Register("TileBackgroundId", typeof(int), typeof(MapTileBackgroundPreview),
    new PropertyMetadata(0, new PropertyChangedCallback(TileBackgroundIdChanged)));

public int TileBackgroundId
{
    get
    {
        return (int)GetValue(TileBackgroundIdProperty);
    }
    set
    {
        SetValue(TileBackgroundIdProperty, value);
    }
}

private static void TileBackgroundIdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    MapTileBackgroundPreview ctrl = (MapTileBackgroundPreview)d;
    ctrl.TileBackground = TMapTileBackgroundTool.Get((int)e.NewValue);
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM