WPF DataContext没有通过自定义控件转发到控件树中

时间:2019-01-13 03:07:31

标签: c# wpf data-binding viewbox

我需要一个类似于Viewbox的布局控件,但拉伸规则略有不同。我从.NET参考源复制了Viewbox源,但是现在我在子控件中看到了奇怪的数据绑定问题。

这是一个简化的示例:

<Window
    x:Class="CustomViewbox.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:CustomViewbox"
    Title="MainWindow" Height="450" Width="800"
>
    <local:Viewbox Stretch="Uniform">
        <Grid>
            <StackPanel>
                <TextBlock Foreground="Red" Margin="5" Text="{Binding Path=Round,StringFormat={}Round {0}}"/>
                <Button Margin="5" Content="Next round" Click="next_round_Click"/>
            </StackPanel>
        </Grid>
    </local:Viewbox>
</Window>

以及相应的代码隐藏:

using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace CustomViewbox
{
    public class Game : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }

        int round = 1;
        public int Round { get { return round; } set { round = value; NotifyPropertyChanged(); } }
    }

    public partial class MainWindow : Window
    {
        Game game;

        public MainWindow()
        {
            InitializeComponent();

            game = new Game();
            DataContext = game;
        }

        private void next_round_Click(object sender, RoutedEventArgs e)
        {
            ++game.Round;
        }
    }
}

运行时,TextBlock为空或不可见(不会折叠)。到目前为止,我发现了3种无法令人满意的解决方法:

  • 如果仅用Viewbox替换local:Viewbox,则布局错误
  • 如果我从TextBlock中删除了Foreground属性,则文本颜色错误
  • 最后,如果我将属性DataContext =“ {Binding}”添加到TextBlock,它看起来还可以,但是这感觉不像是正确的解决方法

有人可以解释发生了什么,或如何调试吗?

为完整起见,这里是修改后的Viewbox源代码,应该是重现我的问题所需的所有代码:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace CustomViewbox
{
    public class Viewbox : Decorator
    {
        private ContainerVisual _internalVisual;

        public static readonly DependencyProperty StretchProperty = DependencyProperty.Register("Stretch", typeof(Stretch), typeof(Viewbox), new FrameworkPropertyMetadata(Stretch.Uniform, FrameworkPropertyMetadataOptions.AffectsMeasure), new ValidateValueCallback(ValidateStretchValue));
        private static bool ValidateStretchValue(object value)
        {
            var s = (Stretch)value;
            return s == Stretch.Uniform
                || s == Stretch.None
                || s == Stretch.Fill
                || s == Stretch.UniformToFill;
        }

        private ContainerVisual InternalVisual
        {
            get
            {
                if (_internalVisual == null)
                {
                    _internalVisual = new ContainerVisual();
                    AddVisualChild(_internalVisual);
                }
                return _internalVisual;
            }
        }

        private UIElement InternalChild
        {
            get
            {
                VisualCollection vc = InternalVisual.Children;
                if (vc.Count != 0) return vc[0] as UIElement;
                else return null;
            }
            set
            {
                VisualCollection vc = InternalVisual.Children;
                if (vc.Count != 0) vc.Clear();
                vc.Add(value);
            }
        }

        private Transform InternalTransform
        {
            get
            {
                return InternalVisual.Transform;
            }
            set
            {
                InternalVisual.Transform = value;
            }
        }

        public override UIElement Child
        {
            // everything is the same as on Decorator, the only difference is to insert intermediate Visual to specify scaling transform
            get
            {
                return InternalChild;
            }

            set
            {
                UIElement old = InternalChild;

                if (old != value)
                {
                    //need to remove old element from logical tree
                    RemoveLogicalChild(old);

                    if (value != null)
                    {
                        AddLogicalChild(value);
                    }

                    InternalChild = value;

                    InvalidateMeasure();
                }
            }
        }

        protected override int VisualChildrenCount
        {
            get { return 1; /* Always have internal container visual */ }
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index != 0)
            {
                throw new ArgumentOutOfRangeException("index", index, /*SR.Get(SRID.Visual_ArgumentOutOfRange)*/"argument out of range");
            }
            return InternalVisual;
        }

        public Stretch Stretch
        {
            get { return (Stretch)GetValue(StretchProperty); }
            set { SetValue(StretchProperty, value); }
        }

        /// <summary>
        /// Updates DesiredSize of the Viewbox.  Called by parent UIElement.  This is the first pass of layout.
        /// </summary>
        /// <remarks>
        /// Viewbox measures it's child at an infinite constraint; it allows the child to be however large it so desires.
        /// The child's returned size will be used as it's natural size for scaling to Viewbox's size during Arrange.
        /// </remarks>
        /// <param name="constraint">Constraint size is an "upper limit" that the return value should not exceed.</param>
        /// <returns>The Decorator's desired size.</returns>
        protected override Size MeasureOverride(Size constraint)
        {
            var child = InternalChild;
            var parentSize = new Size();

            if (child != null)
            {
                // Initialize child constraint to infinity.  We need to get a "natural" size for the child in absence of constraint.
                // Note that an author *can* impose a constraint on a child by using Height/Width, &c... properties 
                var infiniteConstraint = new Size(double.PositiveInfinity, double.PositiveInfinity);

                child.Measure(infiniteConstraint);
                var childSize = child.DesiredSize;

                var scalefac = ComputeScaleFactor(constraint, childSize, Stretch);

                parentSize.Width = scalefac * childSize.Width;
                parentSize.Height = scalefac * childSize.Height;

                if (parentSize.Width > constraint.Width)
                    parentSize.Width = constraint.Width;

                childSize = new Size(constraint.Width / scalefac, constraint.Height / scalefac);
                child.Measure(childSize);
            }

            return parentSize;
        }

        /// <summary>
        /// Viewbox always sets the child to its desired size.  It then computes and applies a transformation
        /// from that size to the space available: Viewbox's own input size less child margin.
        /// 
        /// Viewbox also calls arrange on its child.
        /// </summary>
        /// <param name="arrangeSize">Size in which Border will draw the borders/background and children.</param>
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            var child = InternalChild;
            if (child != null)
            {
                var childSize = child.DesiredSize;

                // Compute scaling factors from arrange size and the measured child content size
                var scalefac = ComputeScaleFactor(arrangeSize, childSize, Stretch);

                InternalTransform = new ScaleTransform(scalefac, scalefac);

                childSize = new Size(arrangeSize.Width / scalefac, arrangeSize.Height / scalefac);

                // Arrange the child to the desired size 
                child.Arrange(new Rect(new Point(), childSize));

                // return the size occupied by scaled child
                arrangeSize.Width = scalefac * childSize.Width;
                arrangeSize.Height = scalefac * childSize.Height;
            }
            return arrangeSize;
        }

        /// <summary>
        /// This is a helper function that computes scale factors depending on a target size and a content size
        /// </summary>
        /// <param name="availableSize">Size into which the content is being fitted.</param>
        /// <param name="contentSize">Size of the content, measured natively (unconstrained).</param>
        /// <param name="stretch">Value of the Stretch property on the element.</param>
        internal static double ComputeScaleFactor(Size availableSize, Size contentSize, Stretch stretch)
        {
            // Compute scaling factors to use for axes
            var scale = 1.0;

            var isConstrainedHeight = !Double.IsPositiveInfinity(availableSize.Height);

            if (isConstrainedHeight)
            {
                // Compute scaling factors for both axes
                scale = (DoubleUtil.IsZero(contentSize.Height)) ? 0.0 : availableSize.Height / contentSize.Height;
            }

            return scale;
        }
    }

    static class DoubleUtil
    {
        internal const double DBL_EPSILON = 2.2204460492503131e-016; /* smallest such that 1.0+DBL_EPSILON != 1.0 */
        internal const float FLT_MIN = 1.175494351e-38F; /* Number close to zero, where float.MinValue is -float.MaxValue */

        public static bool IsZero(double value)
        {
            return Math.Abs(value) < 10.0 * DBL_EPSILON;
        }
    }
}

1 个答案:

答案 0 :(得分:0)

事实证明,问题是Klaus Gütter建议在Viewbox类中缺少LogicalChildren的替代。我错误地认为一个人不能覆盖一个内部属性,所以我删除了它。但是事实证明,只要我可以从覆盖中删除内部关键字,就可以。

这是工作代码:

using System;
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace CustomViewbox
{
    /// <summary>
    /// Returns an Enumerator that enumerates over nothing.
    /// </summary>
    internal class EmptyEnumerator : IEnumerator
    {
        // singleton class, private ctor
        private EmptyEnumerator()
        {
        }

        /// <summary>
        /// Read-Only instance of an Empty Enumerator.
        /// </summary>
        public static IEnumerator Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new EmptyEnumerator();
                }
                return _instance;
            }
        }

        /// <summary>
        /// Does nothing.
        /// </summary>
        public void Reset() { }

        /// <summary>
        /// Returns false.
        /// </summary>
        /// <returns>false</returns>
        public bool MoveNext() { return false; }


#pragma warning disable 1634, 1691  // about to use PreSharp message numbers - unknown to C#

        /// <summary>
        /// Returns null.
        /// </summary>
        public object Current
        {
            get
            {
#pragma warning disable 6503 // "Property get methods should not throw exceptions."

                throw new InvalidOperationException();

#pragma warning restore 6503
            }
        }
#pragma warning restore 1634, 1691

        private static IEnumerator _instance;
    }

    internal class SingleChildEnumerator : IEnumerator
    {
        internal SingleChildEnumerator(object Child)
        {
            _child = Child;
            _count = Child == null ? 0 : 1;
        }

        object IEnumerator.Current
        {
            get { return (_index == 0) ? _child : null; }
        }

        bool IEnumerator.MoveNext()
        {
            _index++;
            return _index < _count;
        }

        void IEnumerator.Reset()
        {
            _index = -1;
        }

        private int _index = -1;
        private int _count = 0;
        private object _child;
    }

    /// <summary>
    /// </summary>
    public class Viewbox : Decorator
    {

        //-------------------------------------------------------------------
        //
        //  Constructors
        //
        //-------------------------------------------------------------------

        #region Constructors

        /*static Viewbox()
        {
            ControlsTraceLogger.AddControl(TelemetryControls.ViewBox);
        }*/

        /// <summary>
        ///     Default DependencyObject constructor
        /// </summary>
        /// <remarks>
        ///     Automatic determination of current Dispatcher. Use alternative constructor
        ///     that accepts a Dispatcher for best performance.
        /// </remarks>
        public Viewbox() : base()
        {
        }

        #endregion


        //-------------------------------------------------------------------
        //
        //  Public Fields
        //
        //-------------------------------------------------------------------

        #region Public Fields

        /// <summary>
        /// This is the DependencyProperty for the Viewbox's Stretch property.
        ///
        /// Default:  Stretch.Uniform
        /// <seealso cref="Viewbox.Stretch" />
        /// </summary>
        public static readonly DependencyProperty StretchProperty
            = DependencyProperty.Register(
                "Stretch",          // Property name
                typeof(Stretch),    // Property type
                typeof(Viewbox),    // Property owner
                new FrameworkPropertyMetadata(Stretch.Uniform, FrameworkPropertyMetadataOptions.AffectsMeasure),
                new ValidateValueCallback(ValidateStretchValue));

        private static bool ValidateStretchValue(object value)
        {
            Stretch s = (Stretch)value;
            return (s == Stretch.Uniform
                    || s == Stretch.None
                    || s == Stretch.Fill
                    || s == Stretch.UniformToFill);
        }

        #endregion


        //-------------------------------------------------------------------
        //
        //  Public Methods
        //
        //-------------------------------------------------------------------

        //-------------------------------------------------------------------
        //
        //  Public Properties
        //
        //-------------------------------------------------------------------

        #region Public Properties

        private ContainerVisual InternalVisual
        {
            get
            {
                if (_internalVisual == null)
                {
                    _internalVisual = new ContainerVisual();
                    AddVisualChild(_internalVisual);
                }
                return _internalVisual;
            }
        }

        private UIElement InternalChild
        {
            get
            {
                VisualCollection vc = InternalVisual.Children;
                if (vc.Count != 0) return vc[0] as UIElement;
                else return null;
            }
            set
            {
                VisualCollection vc = InternalVisual.Children;
                if (vc.Count != 0) vc.Clear();
                vc.Add(value);
            }
        }

        private Transform InternalTransform
        {
            get
            {
                return InternalVisual.Transform;
            }
            set
            {
                InternalVisual.Transform = value;
            }
        }

        /// <summary>
        /// The single child of a <see cref="Viewbox" />
        /// </summary>
        public override UIElement Child
        {
            //everything is the same as on Decorator, the only difference is to insert intermediate Visual to
            //specify scaling transform
            get
            {
                return InternalChild;
            }

            set
            {
                UIElement old = InternalChild;

                if (old != value)
                {
                    //need to remove old element from logical tree
                    RemoveLogicalChild(old);

                    if (value != null)
                    {
                        AddLogicalChild(value);
                    }

                    InternalChild = value;

                    InvalidateMeasure();
                }
            }
        }

        /// <summary>
        /// Returns the Visual children count.
        /// </summary>
        protected override int VisualChildrenCount
        {
            get { return 1; /* Always have internal container visual */ }
        }

        /// <summary>
        /// Returns the child at the specified index.
        /// </summary>
        protected override Visual GetVisualChild(int index)
        {
            if (index != 0)
            {
                throw new ArgumentOutOfRangeException(/*"index", index, SR.Get(SRID.Visual_ArgumentOutOfRange)*/);
            }
            return InternalVisual;
        }

        /// <summary>
        /// Returns enumerator to logical children.
        /// </summary>
        protected override IEnumerator LogicalChildren
        {
            get
            {
                if (InternalChild == null)
                {
                    return EmptyEnumerator.Instance;
                }

                return new SingleChildEnumerator(InternalChild);
            }
        }

        /// <summary>
        /// Gets/Sets the Stretch mode of the Viewbox, which determines how the content will be
        /// fit into the Viewbox's space.
        ///
        /// </summary>
        /// <seealso cref="Viewbox.StretchProperty" />
        /// <seealso cref="Stretch" />
        public Stretch Stretch
        {
            get { return (Stretch)GetValue(StretchProperty); }
            set { SetValue(StretchProperty, value); }
        }

        #endregion Public Properties

        //-------------------------------------------------------------------
        //
        //  Protected Methods
        //
        //-------------------------------------------------------------------

        #region Protected Methods

        /// <summary>
        /// Updates DesiredSize of the Viewbox.  Called by parent UIElement.  This is the first pass of layout.
        /// </summary>
        /// <remarks>
        /// Viewbox measures it's child at an infinite constraint; it allows the child to be however large it so desires.
        /// The child's returned size will be used as it's natural size for scaling to Viewbox's size during Arrange.
        /// </remarks>
        /// <param name="constraint">Constraint size is an "upper limit" that the return value should not exceed.</param>
        /// <returns>The Decorator's desired size.</returns>
        protected override Size MeasureOverride(Size constraint)
        {
            var child = InternalChild;
            var parentSize = new Size();

            if (child != null)
            {
                // Initialize child constraint to infinity.  We need to get a "natural" size for the child in absence of constraint.
                // Note that an author *can* impose a constraint on a child by using Height/Width, &c... properties 
                var infiniteConstraint = new Size(double.PositiveInfinity, double.PositiveInfinity);

                child.Measure(infiniteConstraint);
                var childSize = child.DesiredSize;

                var scalefac = ComputeScaleFactor(constraint, childSize, Stretch);

                parentSize.Width = scalefac * childSize.Width;
                parentSize.Height = scalefac * childSize.Height;

                if (parentSize.Width > constraint.Width)
                    parentSize.Width = constraint.Width;

                childSize = new Size(constraint.Width / scalefac, constraint.Height / scalefac);
                child.Measure(childSize);
            }

            return parentSize;
        }

        /// <summary>
        /// Viewbox always sets the child to its desired size.  It then computes and applies a transformation
        /// from that size to the space available: Viewbox's own input size less child margin.
        /// 
        /// Viewbox also calls arrange on its child.
        /// </summary>
        /// <param name="arrangeSize">Size in which Border will draw the borders/background and children.</param>
        protected override Size ArrangeOverride(Size arrangeSize)
        {
            var child = InternalChild;
            if (child != null)
            {
                var childSize = child.DesiredSize;

                // Compute scaling factors from arrange size and the measured child content size
                var scalefac = ComputeScaleFactor(arrangeSize, childSize, Stretch);

                InternalTransform = new ScaleTransform(scalefac, scalefac);

                childSize = new Size(arrangeSize.Width / scalefac, arrangeSize.Height / scalefac);

                // Arrange the child to the desired size 
                child.Arrange(new Rect(new Point(), childSize));

                // return the size occupied by scaled child
                arrangeSize.Width = scalefac * childSize.Width;
                arrangeSize.Height = scalefac * childSize.Height;
            }
            return arrangeSize;
        }

        /// <summary>
        /// This is a helper function that computes scale factors depending on a target size and a content size
        /// </summary>
        /// <param name="availableSize">Size into which the content is being fitted.</param>
        /// <param name="contentSize">Size of the content, measured natively (unconstrained).</param>
        /// <param name="stretch">Value of the Stretch property on the element.</param>
        internal static double ComputeScaleFactor(Size availableSize, Size contentSize, Stretch stretch)
        {
            // Compute scaling factors to use for axes
            var scale = 1.0;

            var isConstrainedHeight = !Double.IsPositiveInfinity(availableSize.Height);

            if (isConstrainedHeight)
            {
                // Compute scaling factors for both axes
                scale = (DoubleUtil.IsZero(contentSize.Height)) ? 0.0 : availableSize.Height / contentSize.Height;
            }

            return scale;
        }

        #endregion Protected Methods



        //-------------------------------------------------------------------
        //
        //  Private Fields
        //
        //-------------------------------------------------------------------

        #region Private Fields

        private ContainerVisual _internalVisual;

        #endregion


    }

    static class DoubleUtil
    {
        internal const double DBL_EPSILON = 2.2204460492503131e-016; /* smallest such that 1.0+DBL_EPSILON != 1.0 */
        internal const float FLT_MIN = 1.175494351e-38F; /* Number close to zero, where float.MinValue is -float.MaxValue */

        public static bool IsZero(double value)
        {
            return Math.Abs(value) < 10.0 * DBL_EPSILON;
        }
    }
}