用户控件依赖属性更新多个控件值

Posted

技术标签:

【中文标题】用户控件依赖属性更新多个控件值【英文标题】:User Control Dependency Property Updating Multiple control values 【发布时间】:2021-05-14 15:11:57 【问题描述】:

我觉得我对这个问题的基本问题是绑定一些内部属性。

在用户控件中,我有一个包含线性渐变的矩形。我创建了一个依赖属性,以便能够给出一个值(0 到 1)来指定渐变线在矩形中的位置。为了动态调整这个值,我连接了一个滑块进行测试。

我还添加了一些反馈文本块,让我知道一些数据是如何流动的以及是否流动。从那我可以告诉我从我的用户控件属性绑定到我的 GradientStops.offset 值不起作用。如何让用户控件仅通过更改 RectangleLevel.RectLevel 值来更新矩形渐变?

用户控制 XAML

<UserControl x:Class="RectDynamicGradient.RectangleLevel"
             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:RectDynamicGradient"
             xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
             mc:Ignorable="d" 
             d:DesignHeight="180" d:DesignWidth="80">
    <Grid>
        <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <Rectangle.Fill>
                <LinearGradientBrush>
                    <GradientStop Color="Cyan" Offset="Binding Gradient_top_color, diag:PresentationTraceSources.TraceLevel=High"/>
                    <GradientStop Color="Black" Offset="Binding Gradient_bottom_color, diag:PresentationTraceSources.TraceLevel=High"/>
                </LinearGradientBrush>
            </Rectangle.Fill>
        </Rectangle>
    </Grid>
</UserControl>

用户控制代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace RectDynamicGradient

    /// <summary>
    /// Interaction logic for RectangleLevel.xaml
    /// </summary>
    public partial class RectangleLevel : UserControl
    
        public double Gradient_bottom_color  get; set; 

        public double Gradient_top_color  get; set; 

        public double RectLevel
        
            get  return (double)GetValue(RectLevelProperty); 
            set  SetValue(RectLevelProperty, value); 
        

        // Using a DependencyProperty as the backing store for RectLevel.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RectLevelProperty =
            DependencyProperty.Register("RectLevel", typeof(double), typeof(RectangleLevel), new FrameworkPropertyMetadata(0.0, 
                FrameworkPropertyMetadataOptions.AffectsRender, 
                new PropertyChangedCallback(ChangeLevel), 
                new CoerceValueCallback(CoerceLevel)), 
                new ValidateValueCallback(ValidateLevel));

        public static void ChangeLevel(DependencyObject d, DependencyPropertyChangedEventArgs e)
        
            RectangleLevel t = d as RectangleLevel;
            t.UpdateGradientStops((double)e.NewValue);
        
        public static object CoerceLevel(DependencyObject d, object value)
        
            if (value is string valstring)
            
                if (Double.TryParse(valstring, out double lvl))
                
                    return lvl;
                
            
            if (value is double valdouble)
            
                return valdouble;
            
            throw new Exception();

        
        public static bool ValidateLevel(object value)
        
            double? level = 0;
            if (value is string valstring)
            
                if (Double.TryParse(valstring, out double lvl))
                
                    level = lvl;
                
            
            if (value is double valdouble)
            
                level = valdouble;
            

            if (level.HasValue && level >= 0 && level <= 1)
                return true;
            else
                return false;
        

        public RectangleLevel()
        
            InitializeComponent();
            this.DataContext = this;
        

        private void UpdateGradientStops(double level)
        
            double scale = 0;

            if (level < .5)
            
                scale = level;
            
            else if (level >= .5)
            
                scale = 1 - level;
            

            Gradient_top_color = level;
            Gradient_bottom_color = level + (level * .1 * scale);
        
    

主窗口 XAML

<Window x:Class="RectDynamicGradient.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:local="clr-namespace:RectDynamicGradient"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Slider x:Name="TheSlider" RenderTransformOrigin="0.5,0.5" VerticalAlignment="Center" LargeChange="0.1" Maximum="1" SmallChange="0.01" >
            <Slider.RenderTransform>
                <TransformGroup>
                    <ScaleTransform ScaleY="2"/>
                    <SkewTransform/>
                    <RotateTransform/>
                    <TranslateTransform Y="2"/>
                </TransformGroup>
            </Slider.RenderTransform>
        </Slider>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <StackPanel Orientation="Horizontal" Grid.Column="0">
                <TextBlock Text="SliderValue:"/>
                <TextBlock  HorizontalAlignment="Center" FontSize="32" Text="Binding ElementName=TheSlider, Path=Value"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" Grid.Column="1">
                <TextBlock Text="UC LEVEL Value:"/>
                <TextBlock Grid.Row="1" HorizontalAlignment="Center" FontSize="32" Text="Binding ElementName=MyUserControl, Path=RectLevel"/>
            </StackPanel>
        </Grid>
        <StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Center">
            <local:RectangleLevel x:Name="MyUserControl" Width="80" VerticalAlignment="Stretch" RectLevel="Binding ElementName=TheSlider, Path=Value"/>
        </StackPanel>
    </Grid>
</Window>

主窗口代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace RectDynamicGradient

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    
        public MainWindow()
        
            InitializeComponent();
        
    

【问题讨论】:

请注意,如果您遵守广泛接受的命名约定,您可以大大提高问题的可读性。对类和属性名称使用 PascalCasing。根本不要在名称中使用下划线。 我正在整理一个简单的示例并粘贴我所拥有的。命名约定现在应该更加标准化。 【参考方案1】:

编辑

根据cmets的反馈,进行了改进。

DataContext 删除了这个并添加了相对源到用户控件(命名为“uc”)

将 NotifyProperties 更改为 DependencyProperties

替换数据上下文

<GradientStop Color="Cyan" Offset="Binding ElementName=uc, Path=GradientFilledColor"/>

更新的依赖属性

    /// <summary>
    /// Property to change the GradientEmptyColor (GradientStop offset) of the rectangle by interfacing with the dependency property.
    /// </summary>
    public double GradientEmptyColor
    
        get  return (double)GetValue(GradientEmptyColorProperty); 
        set  SetValue(GradientEmptyColorProperty, value); 
    

    /// <summary>
    /// Using a DependencyProperty as the backing store for GradientEmptyColor.  This enables animation, styling, binding, etc...
    /// </summary>
    public static readonly DependencyProperty GradientEmptyColorProperty =
        DependencyProperty.Register("GradientEmptyColor", typeof(double), typeof(RectangleLevel), new PropertyMetadata(0.0));


    /// <summary>
    /// Property to change the GradientFilledColor (GradientStop offset) of the rectangle by interfacing with the dependency property.
    /// </summary>
    public double GradientFilledColor
    
        get  return (double)GetValue(GradientFilledColorProperty); 
        set  SetValue(GradientFilledColorProperty, value); 
    

    /// <summary>
    /// Using a DependencyProperty as the backing store for GradientFilledColor.  This enables animation, styling, binding, etc...
    /// </summary>
    public static readonly DependencyProperty GradientFilledColorProperty =
        DependencyProperty.Register("GradientFilledColor", typeof(double), typeof(RectangleLevel), new PropertyMetadata(0.0));

DependencyProperty 提供与 INotifyProperty PropertChanged 事件相同的功能(但使用不同的机制)。 正如@Clemens 所提到的,将 datacontext 设置为 self 是一个糟糕的想法,并且当用户控件在整个项目中使用并且 datacontext 设置为其他内容时会中断链接。

我仍然愿意接受有助于支持良好做法和学习的建议。

更新代码

用户控制 XAML

    <UserControl x:Class="RectDynamicGradient.RectangleLevel"
             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:RectDynamicGradient"
             mc:Ignorable="d" 
             d:DesignHeight="180" d:DesignWidth="80" x:Name="uc">
    <Grid>
        <Rectangle HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <Rectangle.Fill>
                <LinearGradientBrush StartPoint=".5,1" EndPoint=".5,0">
                    <GradientStop Color="Cyan" Offset="Binding ElementName=uc, Path=GradientFilledColor"/>
                    <GradientStop Color="Black" Offset="Binding ElementName=uc, Path=GradientEmptyColor"/>
                </LinearGradientBrush>
            </Rectangle.Fill>
        </Rectangle>
    </Grid>
</UserControl>
 

用户控制代码

    using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace RectDynamicGradient

    /// <summary>
    /// Interaction logic for RectangleLevel.xaml
    /// </summary>
    public partial class RectangleLevel : UserControl
    
        /// <summary>
        /// Property to change the GradientEmptyColor (GradientStop offset) of the rectangle by interfacing with the dependency property.
        /// </summary>
        public double GradientEmptyColor
        
            get  return (double)GetValue(GradientEmptyColorProperty); 
            set  SetValue(GradientEmptyColorProperty, value); 
        

        /// <summary>
        /// Using a DependencyProperty as the backing store for GradientEmptyColor.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty GradientEmptyColorProperty =
            DependencyProperty.Register("GradientEmptyColor", typeof(double), typeof(RectangleLevel), new PropertyMetadata(0.0));


        /// <summary>
        /// Property to change the GradientFilledColor (GradientStop offset) of the rectangle by interfacing with the dependency property.
        /// </summary>
        public double GradientFilledColor
        
            get  return (double)GetValue(GradientFilledColorProperty); 
            set  SetValue(GradientFilledColorProperty, value); 
        

        /// <summary>
        /// Using a DependencyProperty as the backing store for GradientFilledColor.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty GradientFilledColorProperty =
            DependencyProperty.Register("GradientFilledColor", typeof(double), typeof(RectangleLevel), new PropertyMetadata(0.0));


        /// <summary>
        /// Property to change the level (GradientStop offsets) of the rectangle by interfacing with the dependency property.
        /// </summary>
        public double RectLevel
        
            get  return (double)GetValue(RectLevelProperty); 
            set  SetValue(RectLevelProperty, value); 
        

        /// <summary>
        /// Using a DependencyProperty as the backing store for RectLevel.  This enables animation, styling, binding, etc...
        /// </summary>
        public static readonly DependencyProperty RectLevelProperty =
            DependencyProperty.Register("RectLevel", typeof(double), typeof(RectangleLevel), new FrameworkPropertyMetadata(0.0, 
                FrameworkPropertyMetadataOptions.AffectsRender, 
                new PropertyChangedCallback(ChangeLevel), 
                new CoerceValueCallback(CoerceLevel)), 
                new ValidateValueCallback(ValidateLevel));

        /// <summary>
        /// PropertyChangedCallback for DependencyProperty RectLevelProperty.
        /// </summary>
        /// <param name="d">Dependency object causing the event.</param>
        /// <param name="e">Change event arguments.</param>
        public static void ChangeLevel(DependencyObject d, DependencyPropertyChangedEventArgs e)
        
            RectangleLevel t = d as RectangleLevel;
            t.UpdateGradientStops((double)e.NewValue);
        
        /// <summary>
        /// CoerceValueCallback for DependencyProperty RectLevelProperty.
        /// </summary>
        /// <param name="d">Dependency object causing the event.</param>
        /// <param name="value">Value being sent to RectLevelProperty be coerced.</param>
        /// <returns></returns>
        public static object CoerceLevel(DependencyObject d, object value)
        
            if (value is string valstring)
            
                if (Double.TryParse(valstring, out double lvl))
                
                    return lvl;
                
            
            if (value is double valdouble)
            
                return valdouble;
            
            throw new Exception();

        
        /// <summary>
        /// ValidateValueCallback for DependencyProperty RectLevelProperty
        /// </summary>
        /// <param name="value">Value being sent to RectLevelProperty be validated.</param>
        /// <returns>True, if valid value between and including 0 and 1.</returns>
        public static bool ValidateLevel(object value)
        
            double? level = 0;
            if (value is string valstring)
            
                if (Double.TryParse(valstring, out double lvl))
                
                    level = lvl;
                
            
            if (value is double valdouble)
            
                level = valdouble;
            

            if (level.HasValue && level >= 0 && level <= 1)
                return true;
            else
                return false;
        
        /// <summary>
        /// Constructor sets DataContext to itself.
        /// </summary>
        public RectangleLevel()
        
            InitializeComponent();
            this.DataContext = this;
        

        /// <summary>
        /// Updates the variables binded to the GradientStops for the rectangle.
        /// </summary>
        /// <param name="level">Level where the GradientStops should be. Valid value is 0 to 1 representing 0 to 100% filled.</param>
        private void UpdateGradientStops(double level)
        
            double scale = 0;

            if (level < .5)
            
                scale = level;
            
            else if (level >= .5)
            
                scale = 1 - level;
            

            GradientFilledColor = level;
            GradientEmptyColor = level + (level * .1 * scale);
        
    

主窗口 XAML

<Window x:Class="RectDynamicGradient.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:local="clr-namespace:RectDynamicGradient"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Slider x:Name="TheSlider" RenderTransformOrigin="0.5,0.5" VerticalAlignment="Center" LargeChange="0.1" Maximum="1" SmallChange="0.01" >
            <Slider.RenderTransform>
                <TransformGroup>
                    <ScaleTransform ScaleY="2"/>
                    <SkewTransform/>
                    <RotateTransform/>
                    <TranslateTransform Y="2"/>
                </TransformGroup>
            </Slider.RenderTransform>
        </Slider>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <StackPanel Orientation="Horizontal" Grid.Column="0">
                <TextBlock Text="SliderValue:"/>
                <TextBlock  HorizontalAlignment="Center" FontSize="32" Text="Binding ElementName=TheSlider, Path=Value"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" Grid.Column="1">
                <TextBlock Text="UC LEVEL Value:"/>
                <TextBlock Grid.Row="1" HorizontalAlignment="Center" FontSize="32" Text="Binding ElementName=MyUserControl, Path=RectLevel"/>
            </StackPanel>
        </Grid>
        <StackPanel Orientation="Horizontal" Grid.Row="2" HorizontalAlignment="Center">
            <local:RectangleLevel x:Name="MyUserControl" Width="80" VerticalAlignment="Stretch" RectLevel="Binding ElementName=TheSlider, Path=Value"/>
        </StackPanel>
    </Grid>
</Window>

【讨论】:

请注意,在 UserControl 中显式设置自己的 DataContext 是一个常见错误。它破坏了控件依赖属性的任何基于 DataContext 的绑定。实现 INPC 也是没有意义的,因为所有属性都可以只是依赖属性,它们有自己的通知机制。 感谢反馈,我不知道 DependencyProperty 提供了 OnPropertyChanged 事件。它必须发生在注册方法和幕后的所有魔法中。感谢您的反馈和指点! 这是与 INotifyPropertyChanged 不同的机制。 再次感谢您的帮助克莱门斯。我花了时间争论是否应该推动这个,但我认为获取信息而不尝试分享解决方案是我自私的行为。我认为我的问题是有效的,我的解决方案解决了这个问题。现在,当我认为它提供了有用的解决方案时,像我这样的其他初学者会认为这个问题是垃圾。如果有人认为有什么不满意的地方,请告诉我以解决差异,我们可以一起帮助其他人。

以上是关于用户控件依赖属性更新多个控件值的主要内容,如果未能解决你的问题,请参考以下文章

WPF 控件模板不根据依赖属性绑定值更改

使用 ControlTemplate 在用户控件上绑定自定义依赖属性

WPF UserControl:使多个链接的依赖项属性保持同步,而不会导致递归循环 堆栈溢出

用户控件中的 WPF 将控件模板的内容设置为依赖属性的值

如何正确绑定到 MVVM 框架中用户控件的依赖属性

[翻译] WPF 中用户控件 DataContext/Binding 和依赖属性的问题