我一直在嘗試UserControl在幾個不同的視圖之間實作具有一些通用功能的 WPF,但沒有成功。這UserControl本質上是一個ListBox帶有一些上一個和下一個按鈕和一個搜索過濾器的。上一個和下一個邏輯很容易復制和粘貼,但是每次過濾都很痛苦,所以將它們全部封裝到自己的UserControland中會非常好ViewModel。
但是我一直在碰壁以使孩子UserControl/ViewModel以兩種方式系結回父VM。
如果孩子UserControl沒有自己的,則此方法有效ViewModel,但是我必須在該邏輯背后的代碼中實作所有功能,這是不吸引人的,但并非不可能。
我把它歸結為一個演示專案- MRE 專案 - ChildVMBindingDemo
我有一個 MainWindow、MainWindowViewModel、MyListBoxControl 和一個 MyListBoxControlViewModel。
MainWindow.xaml 承載 MyListBoxControl,并將兩個系結轉發到 MyListBoxControl 后面代碼中的 DependencyProperty。然后后面的代碼將這些值轉發到 MyListBoxControlViewModel。這顯然是我的問題 - “流量”命中后面的代碼,在子 VM 中設定值,這是一條從那里開始的單向街道。我已經嘗試了所有我能想到的 BindingMode、UpdateSourceTrigger、NotifyOnSourceUpdated 和 NotifyOnTargetUpdated 組合,但都沒有成功。
MainWindow.xaml:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:MyListBoxControl Grid.Column="0"
MyItems="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor},
Path=DataContext.MyItems}"
SelectedMyItem="{Binding
RelativeSource={RelativeSource AncestorType={x:Type Window}, Mode=FindAncestor},
Path=DataContext.SelectedMyItem}"
/>
</Grid>
MainWindow.xaml.cs:
private readonly MainWindowViewModel _viewModel;
public MainWindow()
{
InitializeComponent();
_viewModel = new MainWindowViewModel();
this.DataContext = _viewModel;
}
MainWindowViewModel.cs:
public MainWindowViewModel()
{
MyItems = new ObservableCollection<MyItem>()
{
new MyItem() { Name = "One" },
new MyItem() { Name = "Two" },
new MyItem() { Name = "Thee" },
new MyItem() { Name = "Four" },
};
}
private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
get => _myItems;
set => Set(ref _myItems, value);
}
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
System.Diagnostics.Debug.WriteLine($"Main View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
MyListBoxControl.xaml:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
ItemsSource="{Binding MyItems}"
SelectedItem="{Binding SelectedMyItem}"
SelectedIndex="{Binding SelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{Binding PrevCommand}"
>Prev</Button>
<Button Grid.Column="2"
Command="{Binding NextCommand}"
>Next</Button>
</Grid>
</Grid>
MyListBoxControl.xaml.cs:
private readonly MyListBoxControlViewModel _viewModel;
public MyListBoxControl()
{
InitializeComponent();
_viewModel = new MyListBoxControlViewModel();
this.DataContext = _viewModel;
}
public static readonly DependencyProperty MyItemsProperty =
DependencyProperty.Register("MyItems", typeof(ObservableCollection<MyItem>), typeof(MyListBoxControl),
new FrameworkPropertyMetadata(null, MyItemsChangedCallback));
public ObservableCollection<MyItem> MyItems
{
get => (ObservableCollection<MyItem>)GetValue(MyItemsProperty);
set
{
SetValue(MyItemsProperty, value);
_viewModel.MyItems = MyItems;
}
}
private static void MyItemsChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyListBoxControl myListBoxControl)
{
myListBoxControl.MyItems = (ObservableCollection<MyItem>)e.NewValue;
}
}
public static readonly DependencyProperty SelectedMyItemProperty =
DependencyProperty.Register(nameof(SelectedMyItem), typeof(MyItem), typeof(MyListBoxControl),
new FrameworkPropertyMetadata(null, SelectedMyItemChangedCallback)
{
BindsTwoWayByDefault = true,
DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
public MyItem SelectedMyItem
{
get => (MyItem)GetValue(SelectedMyItemProperty);
set
{
SetValue(SelectedMyItemProperty, value);
_viewModel.SelectedMyItem = SelectedMyItem;
}
}
private static void SelectedMyItemChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is MyListBoxControl myListBoxControl)
{
myListBoxControl.SelectedMyItem = (MyItem)e.NewValue;
}
}
最后
MyListBoxControlViewModel.cs:
private ObservableCollection<MyItem> _myItems;
public ObservableCollection<MyItem> MyItems
{
get => _myItems;
set => Set(ref _myItems, value);
}
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
System.Diagnostics.Debug.WriteLine($"Child View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
private int _selectedIndex;
public int SelectedIndex
{
get => _selectedIndex;
set => Set(ref _selectedIndex, value);
}
private ICommand _prevCommand;
public ICommand PrevCommand => _prevCommand ?? (_prevCommand = new RelayCommand((param) => Prev(), (param) => CanPrev()));
public bool CanPrev() => SelectedIndex > 0;
private void Prev()
{
SelectedIndex--;
}
private ICommand _nextCommand;
public ICommand NextCommand => _nextCommand ?? (_nextCommand = new RelayCommand((param) => Next(), (param) => CanNext()));
public bool CanNext() => MyItems != null ? SelectedIndex < (MyItems.Count - 1) : false;
private void Next()
{
SelectedIndex ;
}
在我們的專案中有與此類似的預先存在的示例(代碼中的系結將值傳遞給子 VM) - 所以其他人也為此苦苦掙扎,看起來他們的解決方案很簡單,子控制元件永遠不會向父母報告——他們只輸出了一些交易。
我唯一能真正想到的是使用 Messenger 將選定的值直接發送回父級,或者讓子 VMAction呼叫并在依賴屬性后面的代碼中設定新值 - 但任何一個選項都只是尖叫有異味的意大利面條,可能還有一個無限的 setter 回圈/堆疊溢位例外。
這里有更好的方法嗎,或者這里有什么我只是想念的東西?
uj5u.com熱心網友回復:
控制元件永遠不應依賴于顯式或內部視圖模型。它必須單獨依賴于它自己的成員,比如公共屬性。然后資料背景關系可以稍后系結到這個公共屬性。
這將實作獨立于實際DataContext型別的可重用性,并消除了將值委托給私有視圖模型所必需的冗余代碼(和冗余復雜性)。
MVVM 并不意味著每個控制元件都必須有自己的專用視圖模型。它旨在為應用程式提供一個結構。MVVM 的目標是應用程式級設計,而不是控制級設計。控制元件必須在其自己的視圖代碼中實作其 UI 相關邏輯。這可以在代碼隱藏中或分布在多個類中。這些類將被直接參考(而不是通過資料系結),因為它們共享相同的 MVVM 背景關系。UI 邏輯的 MVVM 背景關系始終是View。
資料系結基本上是一種將View和View Model解耦的技術(允許View Model向 View 發送資料)無需參考它——這對 MVVM 模式至關重要)。資料操作通常發生在View Model中(從View角度來看資料的所有者)。View只會對資料視圖進行操作(例如過濾或排序集合)。但從不直接使用資料。
查看以下示例如何將所有與視圖相關的邏輯移至控制元件。
您的固定和改進(在設計方面)MyListBoxControl可能如下所示:
MyListBoxControl.xaml.cs
public partial class MyListBoxControl : UserControl
{
public static RoutedCommand NextCommand { get; } = new RoutedUICommand("Select next MyItem", "NextCommand", typeof(MyListBoxControl));
public static RoutedCommand PreviousCommand { get; } = new RoutedUICommand("Select previous MyItem", "PreviousCommand", typeof(MyListBoxControl));
public ObservableCollection<MyItem> MyItemsSource
{
get => (ObservableCollection<MyItem>)GetValue(MyItemsSourceProperty);
set => SetValue(MyItemsSourceProperty, value);
}
public static readonly DependencyProperty MyItemsSourceProperty = DependencyProperty.Register(
"MyItemsSource",
typeof(ObservableCollection<MyItem>),
typeof(MyListBoxControl),
new PropertyMetadata(default));
public int SelectedMyItemIndex
{
get => (int)GetValue(SelectedMyItemIndexProperty);
set => SetValue(SelectedMyItemIndexProperty, value);
}
public static readonly DependencyProperty SelectedMyItemIndexProperty = DependencyProperty.Register(
"SelectedMyItemIndex",
typeof(int),
typeof(MyListBoxControl),
new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public MyItem SelectedMyItem
{
get => (MyItem)GetValue(SelectedMyItemProperty);
set => SetValue(SelectedMyItemProperty, value);
}
public static readonly DependencyProperty SelectedMyItemProperty = DependencyProperty.Register(
"SelectedMyItem",
typeof(MyItem),
typeof(MyListBoxControl),
new FrameworkPropertyMetadata(default(MyItem), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public MyListBoxControl()
{
InitializeComponent();
this.CommandBindings.Add(new CommandBinding(NextCommand, ExecuteNextCommand, CanExecuteNextCommand));
this.CommandBindings.Add(new CommandBinding(PreviousCommand, ExecutePreviousCommand, CanExecutePreviousCommand));
}
private void CanExecutePreviousCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = this.MyItems?.Any() ?? false && this.SelectedMyItemIndex > 0;
private void ExecutePreviousCommand(object sender, ExecutedRoutedEventArgs e)
=> this.SelectedMyItemIndex = Math.Max(this.SelectedMyItemIndex - 1, 0);
private void CanExecuteNextCommand(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = this.MyItems?.Any() ?? false && this.SelectedMyItemIndex < this.MyItemsSource.Count - 1;
private void ExecuteNextCommand(object sender, ExecutedRoutedEventArgs e)
=> this.SelectedMyItemIndex = Math.Min(this.SelectedMyItemIndex 1, this.MyItemsSource.Count - 1);
}
MyListBoxControl.xaml
<UserControl>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListBox Grid.Row="0"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MyItemsSource}"
SelectedItem="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedMyItem}"
SelectedIndex="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=SelectedMyItemIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{x:Static local:MyListBoxControl.PreviousCommand}"
Content="Prev" />
<Button Grid.Column="2"
Command="{x:Static local:MyListBoxControl.NextCommand}"
Content="Next" />
</Grid>
</Grid>
</UserControl>
使用示例
主視窗.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<MyListBoxControl MyItemsSource="{Binding MyItems}"
SelectedMyItem="{Binding SelectedMyItem}" />
</Window>
如果您打算添加行為或更改現有 的行為ListBox,擴展ListBox將是更好的選擇。這將允許對其專案進行模板化。
此外,如果您的主要目的是分離視圖和相關邏輯,請始終擴展,Control即不要創建UserControl. 在沒有代碼隱藏檔案的情況下實作控制元件也會感覺更自然。它還將在定制方面提供更大的靈活性。例如,雖然UserControl是 a ContentControl,但它不能托管內容。
uj5u.com熱心網友回復:
它肯定不漂亮,而且聞起來也不好——但如果這是你唯一的選擇,這就是它的作業原理。
我Action在 ViewModel 中添加了一個,以便在后面的代碼中設定 DP——注意它只是呼叫 SetValue,而不是直接設定 SelectedMyItem,這可以防止我擔心的 setter 回圈。
MyListBoxControlViewModel.cs
public Action<MyItem> SelectedSetter { get; set; }
private MyItem _selectedMyItem;
public MyItem SelectedMyItem
{
get => _selectedMyItem;
set
{
if (Set(ref _selectedMyItem, value))
{
SelectedSetter?.Invoke(value);
System.Diagnostics.Debug.WriteLine($"Child View Model Selected Item Set: {SelectedMyItem?.Name}");
}
}
}
和
MyListBoxControl.xmal.cs
public MyListBoxControl()
{
InitializeComponent();
_viewModel = new MyListBoxControlViewModel();
_viewModel.SelectedSetter = (value) => SetValue(SelectedMyItemProperty, value);
this.DataContext = _viewModel;
}
雖然不是很好,但它可能會在有限的使用中起作用。
通過建構式傳入 Action 來說明它在操作中的重要性可能很聰明。
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/478204.html
