简体   繁体   中英

WPF user control not updating path

I have a stripped down WPF example of a problem I am having in a much larger project. I have a user control called "UserControl1". The data context is set to self so I have dependency properties defined in the code-behind.

In the control I have an ItemsControl. In the ItemsSource I have a CompositeCollection that contains a CollectionContainer and a Line. The Line is there just to prove to myself that I am drawing.

I also have an object called "GraphPen" that contains a PathGeometry dependency property. The CollectionContainer of the user control contains an ObservableCollection of these GraphPens.

Now, I have a "MainWindow" to test the user control. In the MainWindow I have a DispatchTimer and in the Tick event of that timer, I add LineSegments to a PathFigure which has been added to the Figures collection of the PathGeometry of the single instance of the GraphPen.

I expect to see a diagonal line being drawn in parallel to the existing red line, but nothing shows up. If I put a break point at the end of the Tick event handler, I can examine the user control and drill down and see that the line segments do exist. For some reason they are not being rendered. I suspect I have done something wrong in the binding.

I will supply the code below.

GraphPen.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;

namespace WpfExampleControlLibrary
{
    public class GraphPen : DependencyObject
    {
        #region Constructor

        public GraphPen()
        {
            PenGeometry = new PathGeometry();
        }

        #endregion Constructor

        #region Dependency Properties

        // Line Color

        public static PropertyMetadata PenLineColorPropertyMetadata
            = new PropertyMetadata(null);
        public static DependencyProperty PenLineColorProperty
            = DependencyProperty.Register(
                "PenLineColor",
                typeof(Brush),
                typeof(GraphPen),
                PenLineColorPropertyMetadata);
        public Brush PenLineColor
        {
            get { return (Brush)GetValue(PenLineColorProperty); }
            set { SetValue(PenLineColorProperty, value); }
        }

        // Line Thickness

        public static PropertyMetadata PenLineThicknessPropertyMetadata
            = new PropertyMetadata(null);
        public static DependencyProperty PenLineThicknessProperty
            = DependencyProperty.Register(
                "PenLineThickness",
                typeof(Int32),
                typeof(GraphPen),
                PenLineThicknessPropertyMetadata);
        public Int32 PenLineThickness
        {
            get { return (Int32)GetValue(PenLineThicknessProperty); }
            set { SetValue(PenLineThicknessProperty, value); }
        }

        // Pen Geometry

        public static PropertyMetadata PenGeometryMetadata = new PropertyMetadata(null);
        public static DependencyProperty PenGeometryProperty
            = DependencyProperty.Register(
                "PenGeometry",
                typeof(PathGeometry),
                typeof(UserControl1),
                PenGeometryMetadata);

        public PathGeometry PenGeometry
        {
            get { return (PathGeometry)GetValue(PenGeometryProperty); }
            set { SetValue(PenGeometryProperty, value); }
        }

        #endregion Dependency Properties
    }
}

UserControl1.xaml

<UserControl Name="ExampleControl"
             x:Class="WpfExampleControlLibrary.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfExampleControlLibrary"
             DataContext="{Binding RelativeSource={RelativeSource Self}}"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>

        <DataTemplate DataType="{x:Type local:GraphPen}">
            <Path Stroke="{Binding Path=PenLineColor}"
                  StrokeThickness="{Binding Path=PenLineThickness}"
                  Data="{Binding Path=Geometry}">
            </Path>
        </DataTemplate>

    </UserControl.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="40"/>
        </Grid.RowDefinitions>
        <ItemsControl Grid.Column="0" Grid.Row="0">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas Background="Aquamarine">
                        <Canvas.LayoutTransform>
                            <ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5"/>
                        </Canvas.LayoutTransform>
                    </Canvas>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemsSource>
                <CompositeCollection>
                    <CollectionContainer
                        Collection="{
                            Binding Source={RelativeSource Self}, 
                            Path=GraphPens,
                            Mode=OneWay}"/>
                    <Line X1="10" Y1="0" X2="200" Y2="180" Stroke="DarkRed" StrokeThickness="2"/>
                </CompositeCollection>
            </ItemsControl.ItemsSource>
        </ItemsControl>
        <TextBox x:Name="debug" Grid.Column="0" Grid.Row="1" Text="{Binding Path=DebugText}"/>
    </Grid>
</UserControl>

UserControl1.xaml.cs

namespace WpfExampleControlLibrary
{
    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();

            GraphPens = new ObservableCollection<GraphPen>();
        }

        #region Dependency Properties

        // Pens

        public static PropertyMetadata GraphPenMetadata = new PropertyMetadata(null);
        public static DependencyProperty GraphPensProperty
            = DependencyProperty.Register(
                "GraphPens",
                typeof(ObservableCollection<GraphPen>),
                typeof(UserControl1),
                GraphPenMetadata);

        public ObservableCollection<GraphPen> GraphPens
        {
            get { return (ObservableCollection<GraphPen>)GetValue(GraphPensProperty); }
            set { SetValue(GraphPensProperty, value); }
        }

        // Debug Text

        public static PropertyMetadata DebugTextMetadata = new PropertyMetadata(null);
        public static DependencyProperty DebugTextProperty
            = DependencyProperty.Register(
                "DebugText",
                typeof(string),
                typeof(UserControl1),
                DebugTextMetadata);

        public string DebugText
        {
            get { return (string)GetValue(DebugTextProperty); }
            set { SetValue(DebugTextProperty, value); }
        }

        #endregion Dependency Properties
    }
}

MainWindow.xaml

<Window x:Class="POC_WPF_UserControlExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:Exmpl="clr-namespace:WpfExampleControlLibrary;assembly=WpfExampleControlLibrary"
        xmlns:local="clr-namespace:POC_WPF_UserControlExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="550" Width="550">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition />
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>
        <Exmpl:UserControl1 Grid.Column="1" Grid.Row="1" x:Name="myExample"/>
    </Grid>
</Window>

MainWindow.xaml.cs

namespace POC_WPF_UserControlExample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private DispatcherTimer _timer = null;
        private GraphPen _graphPen0 = null;
        private Int32 _pos = 0;
        private PathFigure _pathFigure = null;

        public MainWindow()
        {
            InitializeComponent();

            _graphPen0 = new GraphPen();
            _graphPen0.PenLineColor = Brushes.DarkGoldenrod;
            _graphPen0.PenLineThickness = 2;
            myExample.GraphPens.Add(_graphPen0);

            _timer = new DispatcherTimer();
            _timer.Tick += Timer_Tick;
            _timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
            _timer.Start();
        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            _pos++;
            Point penPoint0 = new Point(_pos, _pos + 20);
            if (_graphPen0.PenGeometry.Figures.Count == 0)
            {
                _pathFigure = new PathFigure();
                _graphPen0.PenGeometry.Figures.Add(_pathFigure);
                _pathFigure.StartPoint = penPoint0;
            }
            else
            {
                LineSegment segment = new LineSegment(penPoint0, false);
                _pathFigure.Segments.Add(segment);
            }
            myExample.DebugText = _pos.ToString();
        }
    }
}

Screen Shot

绘制的线应与红线平行

I did not need INotifyPropertyChanged , and I did not need to recreate PenGeometry . I'm sorry I wasted your time with those ideas.

I've got your code drawing... something. A line that grows. I don't know if it's drawing exactly what you want, but you can figure out that part now that you can see what it is drawing.

First, minor copy/paste error in GraphPen.cs :

    public static DependencyProperty PenGeometryProperty
        = DependencyProperty.Register(
            "PenGeometry",
            typeof(PathGeometry),
            typeof(UserControl1),
            PenGeometryMetadata);

Owner type parameter needs to be GraphPen , not UserControl1 :

            typeof(PathGeometry),
            typeof(GraphPen),
            PenGeometryMetadata);

Second: Binding in UserControl1 . Your binding to Self isn't going to work because Self , in that case, is the CollectionContainer you're binding on. Usually you'd use a source of RelativeSource={RelativeSource AncestorType=UserControl} , but CollectionContainer is not in the visual tree so that doesn't work (real intuitive, huh?). Instead we use a BindingProxy (source to follow):

<UserControl.Resources>
    <!-- ... stuff ... -->

    <local:BindingProxy 
        x:Key="UserControlBindingProxy"
        Data="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}" 
        />
</UserControl.Resources>

And for the collection container...

<CollectionContainer
    Collection="{
        Binding 
        Source={StaticResource UserControlBindingProxy},
        Path=Data.GraphPens,
        Mode=OneWay}"
    />

Notice we're binding to Data.GraphPens ; Data is the target of the proxy.

Also, we need an ItemTemplate for the ItemsControl , because it doesn't know how to display a GraphPen :

<ItemsControl.ItemTemplate>
    <DataTemplate DataType="local:GraphPen">
        <Border >
            <Path
                Data="{Binding PenGeometry}"
                StrokeThickness="{Binding PenLineThickness}"
                Stroke="{Binding PenLineColor, PresentationTraceSources.TraceLevel=None}"
                />
        </Border>
    </DataTemplate>
</ItemsControl.ItemTemplate>

Note PresentationTraceSources.TraceLevel=None . Change None to High and it'll give you a lot of debugging info about the Binding in the VS Output pane. I added that when I was trying to figure out why I was setting PenLineColor to Brushes.Black in the constructor, but it kept coming out DarkGoldenrod at runtime. Can you say duhhh? Duhhh!

Now the binding proxy. What that does is you take any random object you want to use as a DataContext , bind it to Data on a BindingProxy instance defined as a resource, and you've got that DataContext available via a resource that you can get to with a StaticResource . If you're someplace where you can't get to something via the visual tree with RelativeSource , it's an option that you can rely on.

BindingProxy.cs

using System.Windows;

namespace WpfExampleControlLibrary
{
    public class BindingProxy : Freezable
    {
        #region Overrides of Freezable

        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        #endregion

        public object Data
        {
            get { return (object)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataProperty =
            DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
    }
}

And finally, in MainWindow, you need to pass true for isStroked on the LineSegment instances:

LineSegment segment = new LineSegment(penPoint0, true);

Otherwise they're not drawn.

So now it's back in your lap, in warm goldenrod and soothing aquamarine. Ave, imperator , and all that.

Edit by original author!

Thank you Ed for all your effort.

  1. I can't believe I missed the UserControl1 -> GraphPen
  2. The BindingProxy will be very handy
  3. The TraceLevel will also be handy, I had not used that before
  4. I also corrected the DebugText binding and now that works

    I never even noticed that!

  5. In the DataTemplate why did we need to wrap the Path in a Border?

    We don't. Before I got the Path showing, I added that to have something in the template that was guaranteed to be visible . It had a green border originally. I removed those attributes, but forgot to remove the Border itself.

  6. If you look in my Timer_Tick you will note that now all I have to update is adding new segments. Hopefully, this will help performance. Your opinion?

    No idea. I would actually put that segment adding code in GraphPen as AddSegment(Point pt) and AddSegment(float x, float y) => AddSegment(new Point(x,y)); overloads. I have a great allergy to putting logic in event handlers. The most I'll do is toss an if or a try/catch around a non-handler method that does the real work. Then I'd write AddSegment(Point pt) both ways and benchmark one against the other.

I will add my code for completeness:

UserControl1.xaml

<UserControl Name="ExampleControl"
             x:Class="WpfExampleControlLibrary.UserControl1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfExampleControlLibrary"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>

        <local:BindingProxy
            x:Key="UserControlBindingProxy"
            Data="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}"/>

        <DataTemplate DataType="{x:Type local:GraphPen}">
            <Border>
                <Path
                    Data="{Binding PenGeometry}"
                    StrokeThickness="{Binding PenLineThickness}"
                    Stroke="{Binding
                        PenLineColor,
                        PresentationTraceSources.TraceLevel=None}"
                    />
            </Border>
        </DataTemplate>

    </UserControl.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="40"/>
        </Grid.RowDefinitions>
        <ItemsControl Grid.Column="0" Grid.Row="0">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas Background="Aquamarine">
                        <Canvas.LayoutTransform>
                            <ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5"/>
                        </Canvas.LayoutTransform>
                    </Canvas>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemsSource>
                <CompositeCollection>
                    <CollectionContainer
                        Collection="{Binding
                            Source={StaticResource UserControlBindingProxy}, 
                            Path=Data.GraphPens,
                            Mode=OneWay}"/>
                    <Line X1="10" Y1="0" X2="200" Y2="180" Stroke="DarkRed" StrokeThickness="2"/>
                </CompositeCollection>
            </ItemsControl.ItemsSource>
        </ItemsControl>
        <TextBox
            x:Name="debug"
            Grid.Column="0" Grid.Row="1"
            Text="{Binding
                Source={StaticResource UserControlBindingProxy},
                Path=Data.DebugText,
                Mode=OneWay}"/>
    </Grid>
</UserControl>

UserControl1.xaml.cs

namespace WpfExampleControlLibrary
{
    /// <summary>
    /// Interaction logic for UserControl1.xaml
    /// </summary>
    public partial class UserControl1 : UserControl
    {
        #region Constructor

        public UserControl1()
        {
            InitializeComponent();

            GraphPens = new ObservableCollection<GraphPen>();
        }

        #endregion Constructor

        #region Public Methods

        #endregion Public Methods

        #region Dependency Properties

        // Pens

        public static PropertyMetadata GraphPenMetadata = new PropertyMetadata(null);
        public static DependencyProperty GraphPensProperty
            = DependencyProperty.Register(
                "GraphPens",
                typeof(ObservableCollection<GraphPen>),
                typeof(UserControl1),
                GraphPenMetadata);

        public ObservableCollection<GraphPen> GraphPens
        {
            get { return (ObservableCollection<GraphPen>)GetValue(GraphPensProperty); }
            set { SetValue(GraphPensProperty, value); }
        }

        // Debug Text

        public static PropertyMetadata DebugTextMetadata = new PropertyMetadata(null);
        public static DependencyProperty DebugTextProperty
            = DependencyProperty.Register(
                "DebugText",
                typeof(string),
                typeof(UserControl1),
                DebugTextMetadata);

        public string DebugText
        {
            get { return (string)GetValue(DebugTextProperty); }
            set { SetValue(DebugTextProperty, value); }
        }

        #endregion Dependency Properties
    }
}

GraphPen.cs

namespace WpfExampleControlLibrary
{
    public class GraphPen : DependencyObject
    {
        #region Constructor

        public GraphPen()
        {
            PenGeometry = new PathGeometry();
        }

        #endregion Constructor

        #region Dependency Properties

        // Line Color

        public static PropertyMetadata PenLineColorPropertyMetadata
            = new PropertyMetadata(null);
        public static DependencyProperty PenLineColorProperty
            = DependencyProperty.Register(
                "PenLineColor",
                typeof(Brush),
                typeof(GraphPen),
                PenLineColorPropertyMetadata);
        public Brush PenLineColor
        {
            get { return (Brush)GetValue(PenLineColorProperty); }
            set { SetValue(PenLineColorProperty, value); }
        }

        // Line Thickness

        public static PropertyMetadata PenLineThicknessPropertyMetadata
            = new PropertyMetadata(null);
        public static DependencyProperty PenLineThicknessProperty
            = DependencyProperty.Register(
                "PenLineThickness",
                typeof(Int32),
                typeof(GraphPen),
                PenLineThicknessPropertyMetadata);
        public Int32 PenLineThickness
        {
            get { return (Int32)GetValue(PenLineThicknessProperty); }
            set { SetValue(PenLineThicknessProperty, value); }
        }

        // Pen Geometry

        public static PropertyMetadata PenGeometryMetadata = new PropertyMetadata(null);
        public static DependencyProperty PenGeometryProperty
            = DependencyProperty.Register(
                "PenGeometry",
                typeof(PathGeometry),
                typeof(GraphPen),
                PenGeometryMetadata);

        public PathGeometry PenGeometry
        {
            get { return (PathGeometry)GetValue(PenGeometryProperty); }
            set { SetValue(PenGeometryProperty, value); }
        }

        #endregion Dependency Properties
    }
}

BindingProxy.cs

namespace WpfExampleControlLibrary
{
    public class BindingProxy : Freezable
    {
        #region Override Freezable Abstract Parts
        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        #endregion Override Freezable Abstract Parts

        #region Dependency Properties

        // Using a DependencyProperty as the backing store for Data.
        // This enables animation, styling, binding, etc...
        public static PropertyMetadata DataMetadata = new PropertyMetadata(null);
        public static readonly DependencyProperty DataProperty
            = DependencyProperty.Register(
                "Data",
                typeof(object),
                typeof(BindingProxy),
                DataMetadata);

        public object Data
        {
            get { return (object)GetValue(DataProperty); }
            set { SetValue(DataProperty, value); }
        }

        #endregion Dependency Properties
    }
}

MainWindow.xaml

<Window x:Class="POC_WPF_UserControlExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:Exmpl="clr-namespace:WpfExampleControlLibrary;assembly=WpfExampleControlLibrary"
        xmlns:local="clr-namespace:POC_WPF_UserControlExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="550" Width="550">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition />
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="100" />
            <RowDefinition />
            <RowDefinition Height="100" />
        </Grid.RowDefinitions>
        <Exmpl:UserControl1 Grid.Column="1" Grid.Row="1" x:Name="myExample"/>
    </Grid>
</Window>

MainWindow.xaml.cs

namespace POC_WPF_UserControlExample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private DispatcherTimer _timer = null;
        private GraphPen _graphPen0 = null;
        private Int32 _pos = 0;
        private PathFigure _pathFigure0 = null;
        private bool _firstTime = true;

        public MainWindow()
        {
            InitializeComponent();

            _pathFigure0 = new PathFigure();
            _graphPen0 = new GraphPen();
            _graphPen0.PenLineColor = Brushes.DarkGoldenrod;
            _graphPen0.PenLineThickness = 2;
            _graphPen0.PenGeometry = new PathGeometry();
            _graphPen0.PenGeometry.Figures.Add(_pathFigure0);
            myExample.GraphPens.Add(_graphPen0);

            _timer = new DispatcherTimer();
            _timer.Tick += Timer_Tick;
            _timer.Interval = new TimeSpan(0, 0, 0, 0, 100);
            _timer.Start();
        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            _pos++;
            Point penPoint0 = new Point(_pos, _pos + 20);
            if (_firstTime)
            {
                myExample.GraphPens[0].PenGeometry.Figures[0].StartPoint = penPoint0;
                _firstTime = false;
            }
            else
            {
                LineSegment segment = new LineSegment(penPoint0, true);
                myExample.GraphPens[0].PenGeometry.Figures[0].Segments.Add(segment);
            }

            myExample.DebugText = _pos.ToString();
        }
    }
}

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