如何将复选框列表的选择绑定到数据库 WPF MVVM EF

How to bind the selection of CheckBox List to database WPF MVVM EF

我正在尝试创建一个 UserControl 以使用 MVVM 在 WPF 中创建一个 CheckBoxList。此外,Entity Framework 用于部署数据。鉴于以下内容:

WPF(用户控件)

<Grid>
    <ListBox Name="ListBox" ItemsSource="{Binding TheList}" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <CheckBox Content="{Binding Sport}" 
                          Tag="{Binding SportsId}"
                          IsChecked="{Binding IsChecked}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

public class Athlete
{
    public int AthleteId { get; set; }
    public string Name { get; set; }
    public ICollection<Sports> Sports { get; set; }

}

public class Sports {
    public int SportsId { get; set; }
    public string Sport { get; set; }
}

如何让 UserControl 加载整个列表 class 然后 select 运动员可以玩的列表?

这个问题非常宽泛和模糊,但我会尽力解释。您可能需要至少阅读整篇文章两遍。还要阅读 the external link 到结尾 或至少仔细阅读其中的代码。

先看看最终的解决方案:

public class AthleteVM : DependencyObject
{
    public int AthleteId { get; set; }
    public string Name { get; set; }

    private ObservableCollection<SportSelectionVM> _sports = new ObservableCollection<SportSelectionVM>();
    public ObservableCollection<SportSelectionVM> Sports { get { return _sports; } }

}

public class SportSelectionVM : DependencyObject
{
    public int SportsId { get; set; }
    public string Name { get; set; }

    private Model.Sport _model;
    public SportSelectionVM(Model.Sport model, bool isSelected)
    {
        _model = model;
        SportsId = model.Id;
        Name = model.Name;
        IsSelected = isSelected;
    }

    /// <summary>
    /// Gets or Sets IsSelected Dependency Property
    /// </summary>
    public bool IsSelected
    {
        get { return (bool)GetValue(IsSelectedProperty); }
        set { SetValue(IsSelectedProperty, value); }
    }
    public static readonly DependencyProperty IsSelectedProperty =
        DependencyProperty.Register("IsSelected", typeof(bool), typeof(AthleteVM), new PropertyMetadata(false, (d, e) =>
        {
            //  PropertyChangedCallback
            var vm = d as SportSelectionVM;
            var val = (bool)e.NewValue;
            AthleteDataService.UpdateModel(vm._model, val);//database changes here
        }));
}

XAML:

    <ListBox Name="ListBox" ItemsSource="{Binding Sports}" >
        <ListBox.ItemTemplate>
            <DataTemplate>
                <CheckBox Content="{Binding Name}" 
                      Tag="{Binding SportsId}"
                      IsChecked="{Binding IsSelected}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

此视图的 DataContext 是 AthleteVM 的一个实例。将所有运动添加到 AthleteVM 中的 Sports 并在必要时设置 IsSelected

查看构造函数:public SportSelectionVM(Model.Sport model, bool isSelected)

应该使用类似的策略来创建 AthleteVM 或在其父项中填充 AthleteVM 列表。

英孚和伍伦贡大学:

正如我们所知,这是 MVVM 背后的理念:

[Model] <--- [VM] <--TwoWay Binding--> [View]

当EF加入此模式后,通常建议也遵循UOW模式。

通常 UOW (UnitOfWork) 是一个对象,它负责 一个 数据库事务(我不是说 SQLTransaction),建议总是在里面创建一个 UOW using 声明,以便之后处理。使用这种方法,您应该期望 运行 进入这个问题:不同的 UOW 如何相互交互。哪个答案是:他们没有。

每个 UOW 创建数据库的惰性副本并开始修改它,直到您告诉它丢弃或保存。如果在此过程中创建了另一个 UOW,则它不包含对以前的 UOW 所做的任何更改,除非保存了以前的 UOW。

因此您不必担心 Model,相反,您将专注于 DataService 以获得一些东西 like this.

型号<->虚拟机

考虑到所有这些信息,ViewModel 只是使用 DataService 的实例从数据库中获取数据并将它们放入可绑定属性中,然后可观察集合以维护 TwoWay Binding.

但是 VMModel 没有 TwoWay 关系,这意味着任何变化ViewModel 应该反映在 Model 上,然后 手动.

保存在数据库中

我最喜欢的解决方案是充分利用 DependencyPropertyPropertyChangedCallback 特性来告诉 DataService 反映变化:

    public int MyProperty
    {
        get { return (int)GetValue(MyPropertyProperty); }
        set { SetValue(MyPropertyProperty, value); }
    }
    public static readonly DependencyProperty MyPropertyProperty =
        DependencyProperty.Register("MyProperty", typeof(int), typeof(MyViewModel), 
            new PropertyMetadata(0, (d,e)=>
            {
                var vm = d as MyViewModel;
                var val = (int)e.NewValue;//check conditions here
                vm._model.MyProperty = val;//update model
                vm._dataService.Update(vm._model);//update database
            }));

在上面的示例中,class MyViewModel 有一个 _model_dataService 的实例。

我找到了问题的解决方案。我能够找到它 here。它是这样的:

WPF UserControl.xaml

<UserControl x:Class="YourNamespace.CheckBoxList"
             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:YourNamespace"
             mc:Ignorable="d" 
             x:Name="ThisCheckBoxList"
             d:DesignHeight="450" d:DesignWidth="800">
    <ScrollViewer  VerticalScrollBarVisibility="Auto">
        <StackPanel>
            <ItemsControl x:Name="host"
                          ItemsSource="{Binding ElementName=ThisCheckBoxList, Path=ItemsSource}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local:MyCheckBox x:Name="theCheckbox"
                                          DisplayMemberPath="{Binding ElementName=ThisCheckBoxList, Path=DisplayPropertyPath}" 
                                          Unchecked="MyCheckBox_Checked"   
                                          Checked="MyCheckBox_Checked" 
                                          Tag="{Binding Path=.}">
                            <local:MyCheckBox.IsChecked >
                                <MultiBinding Mode="OneWay" >
                                    <MultiBinding.Converter>
                                        <local:IsCheckedValueConverter />
                                    </MultiBinding.Converter>
                                    <Binding Path="."></Binding>
                                    <Binding ElementName="ThisCheckBoxList" Path="SelectedItems"></Binding>
                                    <Binding ElementName="ThisCheckBoxList" Path="DisplayPropertyPath"></Binding>
                                </MultiBinding>
                            </local:MyCheckBox.IsChecked>
                        </local:MyCheckBox>

                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </StackPanel>
    </ScrollViewer>
</UserControl>

WPF UserControl.xaml.cs

using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace Eden
{
    /// <summary>
    /// Interaction logic for CheckBoxList.xaml
    /// </summary>
    public partial class CheckBoxList : UserControl
    {
        public CheckBoxList()
        {
            InitializeComponent();
        }

        public object ItemsSource
        {
            get => GetValue(ItemsSourceProperty);
            set => SetValue(ItemsSourceProperty, value);
        }

        // Using a DependencyProperty as the backing store for ItemSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemsSourceProperty =
            DependencyProperty.Register("ItemsSource", typeof(object), typeof(CheckBoxList),
                                        new UIPropertyMetadata(null, (sender, args) => Debug.WriteLine(args)));

        public IList SelectedItems
        {
            get => (IList)GetValue(SelectedItemsProperty);
            set => SetValue(SelectedItemsProperty, value);
        }



        // Using a DependencyProperty as the backing store for SelectedItems.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectedItemsProperty =
            DependencyProperty.Register("SelectedItems", typeof(IList), typeof(CheckBoxList),
                                        new UIPropertyMetadata(null, SelectedChanged));

        /// <summary>
        /// This is called when selected property changed.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="args"></param>
        private static void SelectedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (args.NewValue is INotifyCollectionChanged ncc)
            {
                ncc.CollectionChanged += (sender, e) =>
                {
                    CheckBoxList thiscontrol = (CheckBoxList)obj;
                    RebindAllCheckbox(thiscontrol.host);
                };
            }
        }

        private static void RebindAllCheckbox(DependencyObject de)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(de); i++)
            {
                DependencyObject dobj = VisualTreeHelper.GetChild(de, i);
                if (dobj is CheckBox cb)
                {
                    var bexpression = BindingOperations.GetMultiBindingExpression(cb, MyCheckBox.IsCheckedProperty);
                    if (bexpression != null) bexpression.UpdateTarget();
                }
                RebindAllCheckbox(dobj);
            }
        }



        public string DisplayPropertyPath
        {
            get => (string)GetValue(DisplayPropertyPathProperty);
            set => SetValue(DisplayPropertyPathProperty, value);
        }

        // Using a DependencyProperty as the backing store for DisplayPropertyPath.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DisplayPropertyPathProperty =
            DependencyProperty.Register("DisplayPropertyPath", typeof(string), typeof(CheckBoxList),
                                        new UIPropertyMetadata("", (sender, args) => Debug.WriteLine(args)));

        private PropertyInfo mDisplayPropertyPathPropertyInfo;

        private void MyCheckBox_Checked(object sender, RoutedEventArgs e)
        {
            if (SelectedItems == null)
                return;

            MyCheckBox chb = (MyCheckBox)sender;
            object related = chb.Tag;
            if (mDisplayPropertyPathPropertyInfo == null)
            {

                mDisplayPropertyPathPropertyInfo =
                    related.GetType().GetProperty(
                        DisplayPropertyPath, BindingFlags.Instance | BindingFlags.Public);
            }

            object propertyValue;
            if (DisplayPropertyPath == ".")
                propertyValue = related;
            else
                propertyValue = mDisplayPropertyPathPropertyInfo.GetValue(related, null);

            if (chb.IsChecked == true)
            {
                if (!SelectedItems.Cast<object>()
                         .Any(o => propertyValue.Equals(
                                       DisplayPropertyPath == "." ? o : mDisplayPropertyPathPropertyInfo.GetValue(o, null))))
                {
                    SelectedItems.Add(related);
                }
            }
            else
            {
                object toDeselect = SelectedItems.Cast<object>()
                    .Where(o => propertyValue.Equals(DisplayPropertyPath == "." ? o : mDisplayPropertyPathPropertyInfo.GetValue(o, null)))
                    .FirstOrDefault();
                if (toDeselect != null)
                {
                    SelectedItems.Remove(toDeselect);
                }
            }
        }
    }

    public class MyCheckBox : CheckBox
    {
        public string DisplayMemberPath
        {
            get => (string)GetValue(DisplayMemberPathProperty);
            set => SetValue(DisplayMemberPathProperty, value);
        }

        // Using a DependencyProperty as the backing store for DisplayMemberPath.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DisplayMemberPathProperty =
             DependencyProperty.Register("DisplayMemberPath",
             typeof(string),
             typeof(MyCheckBox),
             new UIPropertyMetadata(string.Empty, (sender, args) =>
             {
                 MyCheckBox item = (MyCheckBox)sender;
                 Binding contentBinding = new Binding((string)args.NewValue);
                 item.SetBinding(ContentProperty, contentBinding);
             }));
    }
}

BaseMultiValueConverter

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;

namespace Eden
{
    /// <summary>
    /// A base value converter that allows direct XAML usage
    /// </summary>
    /// <typeparam name="T">The type of this value converter</typeparam>
    public abstract class BaseMultiValueConverter<T> : MarkupExtension, IMultiValueConverter
        where T : class, new()
    {

        #region Private Variables

        /// <summary>
        /// A single static instance of this value converter
        /// </summary>
        private static T Coverter = null;

        #endregion

        #region Markup Extension Methods
        /// <summary>
        /// Provides a static instance of the value converter
        /// </summary>
        /// <param name="serviceProvider">The service provider</param>
        /// <returns></returns>
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return Coverter ?? (Coverter = new T());
        }

        #endregion

        #region Value Converter Methods

        /// <summary>
        /// The method to convert on type to another
        /// </summary>
        /// <param name="value"></param>
        /// <param name="targetType"></param>
        /// <param name="parameter"></param>
        /// <param name="culture"></param>
        /// <returns></returns>
        public abstract object Convert(object[] value, Type targetType, object parameter, CultureInfo culture);

        /// <summary>
        /// The method to convert a value back to it's source type
        /// </summary>
        /// <param name="value"></param>
        /// <param name="targetType"></param>
        /// <param name="parameter"></param>
        /// <param name="culture"></param>
        /// <returns></returns>
        public abstract object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture);

        #endregion
    }
}

IMultiValueConverter

using EcoDev.Data;
using System;
using System.Collections;
using System.Globalization;
using System.Reflection;
using System.Windows.Data;

namespace Eden
{
    /// <summary>
    /// 
    /// </summary>
    public class IsCheckedValueConverter : BaseMultiValueConverter<IsCheckedValueConverter>
    {
        private PropertyInfo PropertyInfo { get; set; }
        private Type ObjectType { get; set; }

        public override object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (values[1] == null) return false; // IF I do not have no value for selected simply return false

            if (!(values[2] is string PropertyName)) return false;

            if (string.IsNullOrEmpty(PropertyName)) return false;
            if (!targetType.IsAssignableFrom(typeof(bool))) throw new NotSupportedException("Can convert only to boolean");
            IEnumerable collection = values[1] as IEnumerable;
            object value = values[0];
            if (value.GetType() != ObjectType)
            {
                PropertyInfo = value.GetType().GetProperty(PropertyName, BindingFlags.Instance | BindingFlags.Public);
                ObjectType = value.GetType();
            }
            foreach (var obj in collection)
            {
                if (PropertyName == ".")
                {
                    if (value.Equals(obj)) return true;
                }
                else
                {
                    if (PropertyInfo.GetValue(value, null).Equals(PropertyInfo.GetValue(obj, null))) return true;
                }

            }
            return false;
        }


        public override object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

然后你需要做的就是 window/page 你想使用它的地方就是使用这个代码:

<local:CheckBoxList Height="Auto"
                    SelectedItems="{Binding SelectedItems}"
                    ItemsSource="{Binding ItemsSource}"
                    DisplayPropertyPath="Text"/>