Wpf数据上下文绑定使用MVVM模式在视图模型和视图之间

11

我刚开始学习MVVM,以下的问题看起来很基础,但我花了一整天才解决。

我的解决方案有3个项目,一个用于Model,一个用于ViewModel,另一个用于View。 Model包含一个具有Text和CheckStatus两个属性的类。

ViewModel有一个名为listOfItems的列表,其中包含三个项目,每个项目都有来自Model的这两个属性。

View中有一个listView, 其内有一个CheckBox。将CheckBox的内容与Text属性绑定的正确方式是什么?

以下是Model:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;


namespace TheModel
{
public class CheckBoxListModel : INotifyPropertyChanged
{
    private string text;
    public string Text
    {
        get { return text; }
        set
        {
            text = value;
            RaiseChanged("Text");
        }
    }

    private bool checkStatus;
    public bool CheckStatus
    {
        get { return checkStatus; }
        set
        {
            checkStatus = value;
            RaiseChanged("CheckStatus");
        }
    }

    private void RaiseChanged(string propName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
   }
}

这是视图模型

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using TheModel;

namespace TheViewModel
{
public class TheViewModel
{
    public List<CheckBoxListModel> ListOfItems { get; set; }

    public TheViewModelClass()
    {
        ListOfItems = new List<CheckBoxListModel>
        {
        new CheckBoxListModel
        {
            CheckStatus = false,
            Text = "Item 1",
        },
        new CheckBoxListModel
        {
            CheckStatus = false,
            Text = "Item 2",
        },
        new CheckBoxListModel
        {
            CheckStatus = false,
            Text = "Item 3",
        }
    };
    }

    public static implicit operator List<object>(TheViewModelClass v)
    {
        throw new NotImplementedException();
    }
   }
}

这里是视图的XAML代码

 <UserControl
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:ctrl="clr-namespace:TheView.Managers" xmlns:TheViewModel="clr-
 namespace:TheViewModel;assembly=TheViewModel" 
 x:Class="TheView.Styles.ListViewDatabaseStyle">

<UserControl.DataContext>
    <TheViewModel:TheViewModelClass/>
</UserControl.DataContext>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="100"/>
    </Grid.RowDefinitions>
    <Button Content="Continue" Style="{StaticResource ButtonStyle}" 
          Margin="1104,27,40,40"/>
    <ListView x:Name="listView1" SelectionMode="Multiple" 
              Style="{StaticResource ListViewStyle}" Margin="10,55,10,10"
              ctrl:ListViewLayoutManager.Enabled="true" ItemsSource="
          {Binding TheViewModelClass}" >
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Competency Items" 
                  ctrl:ProportionalColumn.Width="1100"/>
            </GridView>
        </ListView.View>
        <ListView.ItemContainerStyle >
            <Style TargetType="{x:Type ListViewItem}">
                <Setter Property="IsSelected" Value="{Binding 
                             CheckedStatus}"/>
                <Setter Property="HorizontalContentAlignment" 
                              Value="Stretch"/>
            </Style>
        </ListView.ItemContainerStyle>
        <ListView.ItemTemplate>
            <DataTemplate>
                <CheckBox  
                     Click="CheckBox_Click"
                     Content="{Binding Path=TheViewModelClass.Text}"
                     IsChecked="{Binding 
                     Path=TheViewModelClass.CheckedStatus}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</Grid>
</UserControl>

这里是代码背后的视图,我知道我不应该在这里放什么,但那部分应该放在哪里?

using System.Windows;
using System.Windows.Controls;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System;
using System.Text;
using TheViewModel;

namespace TheView.Styles
{
public partial class ListViewDatabaseStyle : UserControl
{
    public ListViewDatabaseStyle()
    {
        InitializeComponent();
    }

    public List<string> selectedNames = new List<string>();
    private void CheckBox_Click(object sender, RoutedEventArgs e)
    {
        var ChkBox = sender as CheckBox;
        var item = ChkBox.Content;
        bool isChecked = ChkBox.IsChecked.HasValue ? ChkBox.IsChecked.Value 
         : false;
        if (isChecked)
            selectedNames.Add(item.ToString());
        else
            selectedNames.Remove(item.ToString());
    }
  }
 }

我还没有找到正确的使用DataContext的方法,使其正常工作。我尝试了几次,并且看到它以几种方式实现,但都没有对我起作用。 - user7900863
建议将视图和视图模型放在同一个项目中,因为它们都属于表示层。它们在逻辑上是相互关联的。 - Liero
1
如果我是你,我会将列表更改为ObservableCollection。public ObservableCollection<CheckBoxListModel> ListOfItems { get; set; }相信我,你不必在ViewModel中添加每个绑定。在你的ListView中,ItemSource={Binding ListOfItems} <CheckBox Content="{Binding Text}" IsChecked="{Binding CheckedStatus}" /> - Péter Hidvégi
我现在正在尝试创建ObservableCollection。但是你的意思是我不需要将每个绑定添加到我的ListView和CheckBox和IsChecked中吗?否则它们怎么会“知道”它们要绑定什么? - user7900863
1
顺便说一下 - 你可以在WPF聊天室中寻求帮助。我们在工作时间的大部分平日都会在那里。 - Lynn Crumbling
显示剩余4条评论
3个回答

9

这太荒唐了。

以下是一种更简便的方法,它不需要使用外部库、附加的维护类和接口、几乎不需要任何魔法,并且非常灵活,因为你可以拥有包含其他视图模型的视图模型,并且你可以实例化每一个视图模型,因此你可以向它们传递构造函数参数:

对于主窗口的视图模型:

using Wpf = System.Windows;

public partial class TestApp : Wpf.Application
{
    protected override void OnStartup( Wpf.StartupEventArgs e )
    {
        base.OnStartup( e );
        MainWindow = new MainView();
        MainWindow.DataContext = new MainViewModel( e.Args );
        MainWindow.Show();
    }
}

对于所有其他的视图模型:

这是在MainViewModel.cs文件中:

using Collections = System.Collections.Generic;

public class MainViewModel
{
    public SomeViewModel SomeViewModel { get; }
    public OtherViewModel OtherViewModel { get; }
    public Collections.IReadOnlyList<string> Arguments { get; }
    
    public MainViewModel( Collections.IReadOnlyList<string> arguments )
    {
        Arguments = arguments;
        SomeViewModel = new SomeViewModel( this );
        OtherViewModel = new OtherViewModel( this );
    }
}

这是在 MainView.xaml 中的代码:

[...]
xmlns:local="clr-namespace:the-namespace-of-my-wpf-stuff"
[...]
    <local:SomeView DataContext="{Binding SomeViewModel}" />
    <local:OtherView DataContext="{Binding OtherViewModel}" />
[...]

如您所见,一个视图模型可以简单地成为另一个视图模型的成员(子级);在这种情况下,SomeViewModel和OtherViewModel是MainViewModel的子级。然后,在MainView的XAML文件中,您可以实例化每个子视图,并通过Binding到相应的子视图模型来指定它们的DataContext


3
这是 stack overflow 中最被低估的帖子。我在 Xamarin 中经常这样做,但在 WPF 中由于语法略有不同而无法整合。非常感谢。 - SQLUser
1
@SQLUser 谢谢!C-:= 很高兴能帮上忙。我对大多数 WPF 应用程序编写的复杂方式感到惊讶。 - Mike Nakis
1
谢谢你的回复,我也在阅读其他信息,对于为了将DataContext设置为我的对象而过度复杂化的内容感到印象深刻。 - user0103

5
首先,设置项目的依赖关系。ViewModel 必须可以访问 Model。(View 和 Model 项目不必引用其他项目。) 如果我是你,我会创建一个 StartUp 项目来将控制权转移到 ViewModel。
这个 "StartUp" 项目应该是 WPF,其他所有项目都应该是 "class library",但不要忘记向项目添加所需的引用 (例如,为了创建用户控件,你的视图项目需要 system.xaml)。
项目依赖关系: - StartUp --> ViewModel; (- ViewModel --> View; 或者使用 DI 避免) - ViewModel --> Model; (我应该为接口创建另一个项目,但这只是我的执念。)
启动项目: 现在,在你的启动 (WPF) 项目中,应该包含在 (app.xaml.cs) 中:
protected override void OnStartup(StartupEventArgs e)
{
    // delete the startupuri tag from your app.xaml
    base.OnStartup(e);
    //this MainViewModel from your ViewModel project
    MainWindow = new MainWindow(new MainViewModel());
} 

在您的启动wpf项目中(用于显示您的UserControls),只有一个东西(窗口)。

MainWindow.xaml内容:

<Window x:Class="StartUp.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="MainWindow" WindowState="Maximized" WindowStyle="None" AllowsTransparency="True">
        <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding Control}"/>
</Window>

(以及 xaml.cs)
  public partial class MainWindow : Window
    {
        public MainWindow(INotifyPropertyChanged ViewModel)
        {
            InitializeComponent();
            this.DataContext = ViewModel;
            this.Show();
        }
    }

这就是你的启动WPF项目的全部内容。通过这种方式,我们将控制权交给了你的ViewModel项目。

(好的,这只是额外的内容,但我应该创建一个“ViewService”来处理我的用户控件)

接口用于查找所有视图并将其与ViewModel匹配。

public interface IControlView
{
    INotifyPropertyChanged ViewModel { get; set; }
}

我创建了一个单例来存储和匹配我的视图和视图模型。这部分内容可以跳过。我在Model项目中定义了它。

 public class ViewService<T> where T : IControlView
    {
        private readonly List<WeakReference> cache;

        public delegate void ShowDelegate(T ResultView);
        public event ShowDelegate Show;
        public void ShowControl<Z>(INotifyPropertyChanged ViewModel)
        {
            if (Show != null)
                Show(GetView<Z>(ViewModel));
        }

        #region Singleton

        private static ViewService<T> instance;
        public static ViewService<T> GetContainer
        {
            get
            {
                if (instance == null)
                {
                    instance = new ViewService<T>();
                }
                return instance;
            }
        }

        private ViewService()
        {
            cache = new List<WeakReference>();
            var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(s => s.GetTypes()).Where(r => typeof(T).IsAssignableFrom(r) && !r.IsInterface && !r.IsAbstract && !r.IsEnum);

            foreach (Type type in types)
            {
                cache.Add(new WeakReference((T)Activator.CreateInstance(type)));
            }
        }

        #endregion

        private T GetView<Z>(INotifyPropertyChanged ViewModel)
        {
            T target = default(T);
            foreach (var wRef in cache)
            {
                if (wRef.IsAlive && wRef.Target.GetType().IsEquivalentTo(typeof(Z)))
                {
                    target = (T)wRef.Target;
                    break;
                }
            }

            if(target==null)
                target = (T)Activator.CreateInstance(typeof(Z));

            if(ViewModel != null)
                target.ViewModel = ViewModel;

            return target;
        }

    }

现在您有一个“服务”可以在主窗口中显示来自您的ViewModel的用户控件:

public class MainViewModel : INotifyPropertyChanged
    {

        private IControlView _control;
        public IControlView Control
        {
            get
            {
                return _control;
            }
            set
            {
                _control = value;
                OnPropertyChanged();
            }
        }

        public MainViewModel()
        {   //Subscribe for the ViewService event:   
            ViewService<IControlView>.GetContainer.Show += ShowControl;
            // in this way, here is how to set a user control to the window.
            ViewService<IControlView>.GetContainer.ShowControl<ListViewDatabaseStyle>(new TheViewModel(yourDependencyItems));
           //you can call this anywhere in your viewmodel project. For example inside a command too.
        }

        public void ShowControl(IControlView ControlView)
        {
            Control = ControlView;
        }

        //implement INotifyPropertyChanged...
        protected void OnPropertyChanged([CallerMemberName] string name = "propertyName")
        {
           PropertyChangedEventHandler handler = PropertyChanged;
           if (handler != null)
           {
               handler(this, new PropertyChangedEventArgs(name));
           }
        }

           public event PropertyChangedEventHandler PropertyChanged;
    }

如果您不想使用这个 "ViewService",只需创建一个UserControl实例,将View的DataContext与您的ViewModel匹配,并将此视图赋给Control属性。以下是您的带有列表的ViewModel(仍在ViewMoldel项目中)。
public class TheViewModel
    {
        private readonly ObservableCollection<ISelectable> listOfItems;
        public ObservableCollection<ISelectable> ListOfItems 
        {
            get { return listOfItems; }
        }

        public ICommand SaveCheckedItemsText{
            get{ return new RelayCommand(CollectNamesOfSelectedElements);}
        }

        public IEnumerable<ISelectable> GetSelectedElements
        {
            get { return listOfItems.Where(item=>item.CheckStatus); }
        }

        public TheViewModel(IList<ISelectable> dependencyItems)
        {
            listOfItems= new ObservableCollection<ISelectable>(dependencyItems);
        }

        //here is your list...
        private List<string> selectedNames

        //use this...
        private void CollectNamesOfSelectedElements()
        {
           selectedNames = new List<string>();
           foreach(ISelectable item in GetSelectedElements)
           {
             //you should override the ToString in your model if you want to do this...
             selectedNames.Add(item.ToString());
           }
        }

    }

RelayCommand 文章

视图:(在这里放置您的所有用户控件。)

在您的用户控件(XAML)中:

<UserControl x:Class="View.ListViewDataStyle"
             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" namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
             mc:Ignorable="d">
<Button Command={Binding SaveCheckedItemsText}/>
<!-- Another content -->
    <ListView ItemsSource="{Binding ListOfItems}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <CheckBox Content="{Binding Text}" IsChecked="{Binding CheckedStatus}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</UserControl>

这里是与界面相关的XAML.CS代码(用于用户控件):

public partial class ListViewDatabaseStyle : UserControl, IControlView
    {
        public ListViewDatabaseStyle ()
        {
            InitializeComponent();
        }

        public INotifyPropertyChanged ViewModel
        {
            get
            {
                return (INotifyPropertyChanged)DataContext;
            }
            set
            {
                DataContext = value;
            }
        }
    }

最后一个是与你的模型相关的模型项目:

 public interface ISelectable
    {
        bool CheckStatus { get; set; }
    }

public class CheckBoxListModel : INotifyPropertyChanged, ISelectable
{
    private string text;
    public string Text
    {
        get { return text; }
        set
        {
            text = value;
            RaiseChanged("Text");
        }
    }

    private bool checkStatus;
    public bool CheckStatus
    {
        get { return checkStatus; }
        set
        {
            checkStatus = value;
            RaiseChanged("CheckStatus");
        }
    }

    private void RaiseChanged(string propName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
   }
}

对于英语语法错误,请谅解,我希望你能理解我的帖子。

更新: 使用DI技术避免从ViewModel引用View。依赖注入服务将通过构造函数注入正确的对象。


1
非常感谢Peter提供的详细解释。我会尝试做同样的事情,相信这次一定会成功。再次感谢。 :) - user7900863
感谢你的帮助,彼得。但是经过这一切并将所有酷炫的东西添加到软件中后,它仍然不能做我想要它做的简单事情,即让带有复选框的列表显示多个项目,并使用按钮将选中的项目复制到另一个列表中...就是这么简单 :) - user7900863
没问题。我只是感到惊讶。我以为我犯了一些错误。 - Péter Hidvégi
视图模型绝对不必引用视图。它可以为与视图相关的任何内容公开接口,而这些接口的实现可以由视图本身提供。 - user1228
我在理解这个答案时遇到了困难,它非常复杂。 - Zimano
显示剩余10条评论

1
<UserControl.DataContext>
    <TheViewModel:TheViewModelClass/>
</UserControl.DataContext>

<ListView ItemsSource="{Binding ListOfItems}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <CheckBox Content="{Binding Text}" IsChecked="{Binding CheckedStatus}" />
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

这个不起作用,和Peter建议的一样。它显示为:TheModel.CheckBoxListModel TheModel.CheckBoxListModel TheModel.CheckBoxListModel - user7900863
1
但这应该是它的工作方式。问题可能不在你的xaml中,而是你没有向我们展示的代码中。我看到你在复制和粘贴时已经操纵了代码,因为你的viewmodel中有错误。不要紧,这是正确的做法。调试应用程序时,请检查输出窗口。顺便问一下,你修改了CheckBox的ControlTemplate吗? - Liero
是的,我已经修改了它,代码也不同,因为我正在对其进行副本工作。希望在得到一些帮助后,我能够理解为什么它不能正常工作 :) 谢谢大家。 - user7900863

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接