UWP 自定义控件绑定在任一方向均不起作用
UWP Custom control binding not working in either direction
我正在使用 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; }
}
我做了一些调查,这是我的发现:
- 如果
NumberPicker
派生自ItemsControl
,双向绑定将不起作用。如果它改为派生自 UserControl
,则双向绑定将起作用。
- 看起来您最初将
NumberPicker
创建为 UserControl
(通过右键单击项目 > 添加 > 新项目 > 用户控件),但随后将基础 class 从UserControl
到 ItemsControl
。虽然这不一定是一件坏事,但在这种情况下,它似乎会破坏双向绑定(最终是因为 Application.LoadComponent()
调用它在构造函数的 InitializeComponent()
内部进行)。相反,您应该创建一个 Templated Control,它通过创建一个 .cs 代码文件来工作,并且控件 DefaultStyle
的 XAML 将进入 Themes/Generic.xaml.如果您以这种方式组织控件,双向绑定应该可以工作。
关于视图模型,我还想指出几件事:
- 您的视图模型 class 未实现
INotifyPropertyChanged
,这意味着对视图模型属性的更改不会传播到 NumberPicker
。如果您希望发生这种情况,绑定源(视图模型)必须通过 INotifyPropertyChanged
接口支持 属性 更改事件,或者 属性 必须是 DependencyProperty
.
- 如果您确实实施了
INotifyPropertyChanged
,那么您还需要更新 NumberPicker
class 以更新视图以响应 SelectedValue
[=53] 中的更改=],你目前没有这样做。在这种情况下,您只是在控件上引发 SelectionChanged
事件,您需要滚动 ScrollViewer
以匹配新值。
我正在使用 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; }
}
我做了一些调查,这是我的发现:
- 如果
NumberPicker
派生自ItemsControl
,双向绑定将不起作用。如果它改为派生自UserControl
,则双向绑定将起作用。 - 看起来您最初将
NumberPicker
创建为UserControl
(通过右键单击项目 > 添加 > 新项目 > 用户控件),但随后将基础 class 从UserControl
到ItemsControl
。虽然这不一定是一件坏事,但在这种情况下,它似乎会破坏双向绑定(最终是因为Application.LoadComponent()
调用它在构造函数的InitializeComponent()
内部进行)。相反,您应该创建一个 Templated Control,它通过创建一个 .cs 代码文件来工作,并且控件DefaultStyle
的 XAML 将进入 Themes/Generic.xaml.如果您以这种方式组织控件,双向绑定应该可以工作。
关于视图模型,我还想指出几件事:
- 您的视图模型 class 未实现
INotifyPropertyChanged
,这意味着对视图模型属性的更改不会传播到NumberPicker
。如果您希望发生这种情况,绑定源(视图模型)必须通过INotifyPropertyChanged
接口支持 属性 更改事件,或者 属性 必须是DependencyProperty
. - 如果您确实实施了
INotifyPropertyChanged
,那么您还需要更新NumberPicker
class 以更新视图以响应SelectedValue
[=53] 中的更改=],你目前没有这样做。在这种情况下,您只是在控件上引发SelectionChanged
事件,您需要滚动ScrollViewer
以匹配新值。