【问题标题】:WPF DataContext not forwarded through a custom control down the control treeWPF DataContext 未通过控件树下的自定义控件转发
【发布时间】:2019-01-13 03:07:31
【问题描述】:

我需要一个类似于 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;
        }
    }
}

【问题讨论】:

  • 我将您的代码与参考源进行了比较 - 一个显着的区别是您没有覆盖 LogicalChildren 属性。
  • 这些“略有不同的拉伸规则”到底是什么?也许有更简单的方法来实现它们。
  • 详细阐述克劳斯的观点。 DataContext 被标记为继承,因此沿逻辑树继承。覆盖 LogicalChildren 听起来可能有必要让该过程正常工作。 IIRC 任何孩子的 DP 也必须标记为 Inherits 才能使继承工作。我从来没有发现自己需要对视图框进行子类型化。常规视图框有什么“错误”?
  • 我认为我需要一个自定义视图框,因为我想保留孩子所需的高度(因此它垂直适合窗口)但忽略其所需的宽度(我想要统一缩放,并且孩子宽度应该适应窗口纵横比)。
  • @KlausGütter 我删除了 LogicalChildren 覆盖,因为它是内部的。我不能在我自己的班级中覆盖它,对吧?

标签: c# wpf data-binding viewbox


【解决方案1】:

事实证明,正如Klaus Gütter 所建议的那样,问题在于 Viewbox 类中缺少对 LogicalChildren 的覆盖。我错误地认为不能覆盖内部属性,所以我删除了它。但事实证明,只要我从覆盖中删除 internal 关键字,我就可以。

这是工作代码:

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;
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-08
    • 2013-10-01
    • 2015-11-04
    • 1970-01-01
    • 1970-01-01
    • 2013-05-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多