Мне нужен элемент управления макетом, похожий на Viewbox, но с немного другими правилами растяжения. Я скопировал источники Viewbox из справочного источника .NET, но теперь вижу странные проблемы с привязкой данных в дочерних элементах управления.
Вот упрощенный пример:
<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 неудовлетворительных обходных пути:
- если я заменил local: Viewbox просто Viewbox, неправильная раскладка
- если я удалю атрибут Foreground из TextBlock, текст будет неправильного цвета
- наконец, если я добавлю атрибут 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;
}
}
}