UWP自定义控件绑定无法在任一方向上工作

时间:2016-10-06 13:15:47

标签: c# xaml user-controls uwp

我正在使用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; }
    }

1 个答案:

答案 0 :(得分:1)

我做了一些调查,这是我发现的:

  • 如果NumberPicker来自ItemsControl,则双向绑定将不起作用。如果它来自UserControl,那么双向绑定将起作用。
  • 您最初创建NumberPicker作为UserControl(通过右键单击项目&gt;添加&gt;新项目&gt;用户控件),但随后更改了基类{{1到UserControl。虽然这不一定是坏事,但在这种情况下,它似乎打破了双向绑定(最终是因为ItemsControl调用它在构造函数中的Application.LoadComponent()内执行)。相反,您应该创建一个模板化控件,它通过创建.cs代码文件来工作,控件的InitializeComponent()的XAML将进入Themes / Generic.xaml。如果以这种方式组织控件,双向绑定应该有效。

关于视图模型,还有一些我想指出的事项:

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