UWP自定义控件绑定在任何方向上都不起作用

本文关键字:不起作用 方向 任何 自定义控件 绑定 UWP | 更新日期: 2023-09-27 18:11:56

我正在UWP中的自定义数字选择器控件上工作,我正在尝试将视图模型绑定到其SelectedValue属性。目前,即使使用双向绑定和更新触发器设置为PropertyChanged,我的绑定也不能在任何方向上工作。目前,我已经使用事件处理程序来解决这个问题,但是我想将这个控件分解成一个库,用于我们公司的自定义控件,并使它可以开箱即用。以下是我的控件代码和我在

中使用控件的页面的基本代码:

NumberPicker.xaml:

<ItemsControl
    x:Class="UWPApp.Scorekeeper.NumberPicker"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UWPApp.Scorekeeper"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vms="using:UWPApp.Scorekeeper.Models.ViewModels"
    mc:Ignorable="d"
    x:Name="Select" 
    Loaded="Select_Loaded" 
    ItemsSource="{x:Bind ItemsCollection}"
    d:DesignHeight="300"
    d:DesignWidth="400">
    <ItemsControl.ItemTemplate>
        <DataTemplate x:DataType="local:NumberItem">
            <Viewbox HorizontalAlignment="Stretch" Height="115">
                <TextBlock Text="{x:Bind Value}"></TextBlock>
            </Viewbox>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.Template>
            <ControlTemplate>
                <Grid BorderThickness="4" BorderBrush="Black">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="1*"/>
                        <RowDefinition Height="2*"/>
                        <RowDefinition Height="1*"/>
                    </Grid.RowDefinitions>
                    <Rectangle Opacity=".5">
                        <Rectangle.Fill>
                            <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
                                <GradientStop Offset="0" Color="Black"/>
                                <GradientStop Offset="1" Color="Transparent"/>
                            </LinearGradientBrush>
                        </Rectangle.Fill>
                    </Rectangle>
                    <ScrollViewer Grid.RowSpan="3" ViewChanged="Select_ViewChanged" VerticalSnapPointsType="Mandatory" VerticalSnapPointsAlignment="Center" x:Name="MinutesSelect" HorizontalScrollMode="Disabled" VerticalScrollMode="Auto" VerticalScrollBarVisibility="Visible">
                        <ItemsPresenter></ItemsPresenter>
                    </ScrollViewer>
                    <Rectangle Grid.Row="2" Opacity=".5">
                        <Rectangle.Fill>
                            <LinearGradientBrush StartPoint=".5,1" EndPoint=".5,0">
                                <GradientStop Offset="0" Color="Black"/>
                                <GradientStop Offset="1" Color="Transparent"/>
                            </LinearGradientBrush>
                        </Rectangle.Fill>
                    </Rectangle>
                </Grid>
            </ControlTemplate>
        </ItemsControl.Template>
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Vertical">
            </StackPanel>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

NumberPicker.xaml.cs:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Windows.UI.Core;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using UWPApp.Scorekeeper.Models.ViewModels;
using UWPApp.Scorekeeper.Toolbox;

namespace UWPApp.Scorekeeper
{
    public class NumberItem
{
    public NumberItem(int? value)
    {
        Value = value;
    }
    public int? Value { get; set; }
}
public sealed partial class NumberPicker : ItemsControl
{

    public event SelectionChangedEventHandler SelectionChanged;
    public int RangeBottom { get; set; }
    public int RangeTop { get; set; }
    public static readonly DependencyProperty SelectedValueProperty = DependencyProperty.Register("SelectedValue", typeof(int?), typeof(NumberPicker), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedValueChanged)));
    public static readonly DependencyProperty SelectionChangedProperty = DependencyProperty.Register("SelectionChanged", typeof(SelectionChangedEventHandler), typeof(NumberPicker), new PropertyMetadata(null));
    private static void OnSelectedValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var picker = d as NumberPicker;
        picker.SelectionChanged?.Invoke(picker, new SelectionChangedEventArgs(new List<object> { e.OldValue }, new List<object> { e.NewValue }));
        return;
    }
    public int? SelectedValue { get { return (int?)GetValue(SelectedValueProperty); } set { SetValue(SelectedValueProperty, value); } }
    public ObservableCollection<NumberItem> ItemsCollection { get; set; }
    public NumberPicker()
    {
        this.InitializeComponent();
        DataContext = this;
        ItemsCollection = new ObservableCollection<NumberItem>();
    }
    private void Select_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
    {
        if (!e.IsIntermediate)
        {
            var scroll = sender as ScrollViewer;
            var position = scroll.VerticalOffset;
            var value = Math.Floor(position / 115d);
            SelectedValue = ((int)value);
        }
    }
    private void Select_Loaded(object sender, RoutedEventArgs e)
    {
        var count = RangeTop - RangeBottom + 1;
        var items = Enumerable.Range(RangeBottom, count).Select(m => new NumberItem(m)).ToList();
        foreach (var item in items)
        {
            ItemsCollection.Add(item);
        }
        ItemsCollection.Insert(0, new NumberItem(null));
        ItemsCollection.Add(new NumberItem(null));
        var period = TimeSpan.FromMilliseconds(10);
        Windows.System.Threading.ThreadPoolTimer.CreateTimer(async (source) =>
        {
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                var scroll = Select.FindFirstChild<ScrollViewer>();
                if (SelectedValue != null)
                {
                    var position = SelectedValue * 115d + 81.5;
                    scroll.ChangeView(null, position, null, true);
                }
            });
        }, period);
    }
}
}

Page.xaml:

<Page
    x:Class="UWPApp.Scorekeeper.SelectPenaltyTime"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:UWPApp.Scorekeeper"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vms="using:UWPApp.Scorekeeper.Models.ViewModels"
    mc:Ignorable="d"
    x:Name="PageElement"
    Background="{ThemeResource SystemControlBackgroundAccentBrush}"
    d:DesignHeight="600"
    d:DesignWidth="1024">
    <ContentPresenter x:Name="MainContent" Margin="0,0,0,0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <Grid Background="{ThemeResource SystemControlBackgroundAccentBrush}">
            <Grid.RowDefinitions>
                <RowDefinition Height="110*"/>
                <RowDefinition Height="43*"/>
                <RowDefinition Height="47*"/>
            </Grid.RowDefinitions>
            <local:NumberPicker Margin="100,0,750,0" RangeBottom="0" RangeTop="20" SelectedValue="{Binding ElementName=PageElement,Path=ViewModel.Minutes,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></local:NumberPicker>
        </Grid>
    </ContentPresenter>
</Page>

Page.xaml.cs:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
using UWPApp.Scorekeeper.Models.TransportClasses;
using UWPApp.Scorekeeper.Models.ViewModels;
namespace UWPApp.Scorekeeper
{
    public sealed partial class SelectPenaltyTime : Page
    {
        public GameStateModel StateModel { get; set; }
        public AddPenalty_FVM ViewModel { get; set; }
        public SelectPenaltyTime()
        {
            this.InitializeComponent();
        }
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            var message = e.Parameter as PenaltyMessage;
            StateModel = message.StateModel;
            ViewModel = message.ViewModel;
        }
        private void NumberPicker_Loaded(object sender, RoutedEventArgs e)
        {
            var picker = sender as NumberPicker;
            picker.SelectedValue = ViewModel.Minutes;
        }
        private void NumberPicker_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var picker = sender as NumberPicker;
            ViewModel.Minutes = picker.SelectedValue;
        }
    }
}

AddPenalty_FVM:

public class AddPenalty_FVM
    {
        public int? Minutes { get; set; }
    }

UWP自定义控件绑定在任何方向上都不起作用

我做了一些调查,发现如下:

  • 如果NumberPicker来自ItemsControl,则双向绑定不起作用。如果它是从UserControl派生的,那么双向绑定将工作。
  • 它看起来好像你最初创建的NumberPicker作为UserControl(通过右键单击项目>添加>新建项目>用户控制),但随后改变了基类从UserControlItemsControl。虽然这不一定是一件坏事,但在这种情况下,它似乎破坏了双向绑定(最终是因为它在构造函数的InitializeComponent()内部调用Application.LoadComponent())。相反,您应该创建一个模板控件,它通过创建一个.cs代码文件来工作,并且控件的DefaultStyle的XAML将进入Themes/Generic.xaml。如果您以这种方式组织控件,则双向绑定应该可以工作。

关于视图模型,我还想指出几件事:

  • 你的视图模型类没有实现INotifyPropertyChanged,这意味着对视图模型属性的更改不会传播到NumberPicker。如果你想这样做,绑定源(视图模型)必须通过INotifyPropertyChanged接口支持属性更改事件,或者属性必须是DependencyProperty
  • 如果你确实实现了INotifyPropertyChanged,那么你还需要更新你的NumberPicker类来更新视图以响应SelectedValue属性的变化,这是你目前没有做的。在这种情况下,您只在控件上引发SelectionChanged事件,您需要滚动ScrollViewer以匹配新值。