【问题标题】:MVVM, do I have to keep each command in own class?MVVM,我是否必须将每个命令保留在自己的类中?
【发布时间】:2018-05-10 03:34:57
【问题描述】:

我正在努力适应 MVVM 和 WPF 一个月。我正在尝试做一些基本的事情,但我经常遇到问题。我觉得我通过在线搜索解决了大部分问题。但是现在命令出现了问题。

  1. 问:我看到他们正在使用 RelayCommand、DelegateCommand 或 SimpleCommand。像这样:

    public ICommand DeleteCommand => new SimpleCommand(DeleteProject);
    

尽管我像他们一样创建了所有东西,但我仍然将=> new SimpleCommand(DeleteProject); 部分带红色下划线。

到目前为止,我正在通过为每个命令创建命令类来解决它,但这感觉不是正确的方法。

  1. 问:我还将发布整个项目,我想知道我是否做错了什么或者我应该改进什么。

xaml:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="380" Width="250">
<StackPanel DataContext="{Binding Source={StaticResource gallery}}" Margin="10">
    <ListView DataContext="{Binding Source={StaticResource viewModel}}" 
              SelectedItem="{Binding SelectedGallery}"
              ItemsSource="{Binding GalleryList}"
              Height="150">

        <ListView.View>
            <GridView>
                <GridViewColumn Header="Name" Width="100" DisplayMemberBinding="{Binding Name}"/>
                <GridViewColumn Header="Path" Width="100" DisplayMemberBinding="{Binding Path}"/>
            </GridView>
        </ListView.View>
    </ListView>
    <TextBlock Text="Name" Margin="0, 10, 0, 5"/>
    <TextBox Text="{Binding Name}" />
    <TextBlock Text="Path" Margin="0, 10, 0, 5" />
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="40"/>
        </Grid.ColumnDefinitions>
        <TextBox Text="{Binding Path}" Grid.Column="0"/>
        <Button Command="{Binding Path=ShowFolderClick, Source={StaticResource viewModel}}"
                CommandParameter="{Binding}"
                Content="..." Grid.Column="1" Margin="10, 0, 0, 0"/>
    </Grid>

    <Grid Margin="0, 10, 0, 0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <Button Command="{Binding Path=AddClick, Source={StaticResource viewModel}}" 
                CommandParameter="{Binding}" Content="Add" Grid.Column="0" Margin="15,0,0,0" />
        <Button Command="{Binding Path=DeleteClick, Source={StaticResource viewModel}}"
                Content="Delete" Grid.Column="2" Margin="0,0,15,0" />
    </Grid>
</StackPanel>

型号:

class Gallery : INotifyPropertyChanged
{

    private string _name;
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
            OnPropertyChanged("Name");
        }
    }


    private string _path;

    public string Path
    {
        get
        {
            return _path;
        }
        set
        {
            _path = value;
            OnPropertyChanged("Path");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;


    private void OnPropertyChanged(params string[] propertyNames)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            foreach (string propertyName in propertyNames) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            handler(this, new PropertyChangedEventArgs("HasError"));
        }
    }
}

模型视图:

class GalleryViewModel : INotifyPropertyChanged
{
    public GalleryViewModel()
    {
        GalleryList = new ObservableCollection<Gallery>();
        this.ShowFolderClick = new ShowFolderDialog(this);
        this.AddClick = new AddGalleryCommand(this);
        this.DeleteClick = new DeleteGalleryCommand(this);
    }

    private ObservableCollection<Gallery> _galleryList;

    public ObservableCollection<Gallery> GalleryList
    {
        get { return _galleryList; }
        set { 
            _galleryList = value;
            OnPropertyChanged("GalleryList");
        }
    }

    private Gallery _selectedGallery;

    public Gallery SelectedGallery
    {
        get { return _selectedGallery; }
        set { 
            _selectedGallery = value;
            OnPropertyChanged("SelectedGallery");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(params string[] propertyNames)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            foreach (string propertyName in propertyNames) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            handler(this, new PropertyChangedEventArgs("HasError"));
        }
    }

    public AddGalleryCommand AddClick { get; set; }
    public void AddGalleryClick(Gallery gallery)
    {

        Gallery g = new Gallery();
        g.Name = gallery.Name;
        g.Path = gallery.Path;
        GalleryList.Add(g);

    }

    public DeleteGalleryCommand DeleteClick { get; set; }
    public void DeleteGalleryClick()
    {
        if (SelectedGallery != null)
        {
            GalleryList.Remove(SelectedGallery);
        }
    }

    public ShowFolderDialog ShowFolderClick { get; set; }
    public void ShowFolderDialogClick(Gallery gallery)
    {
        System.Windows.Forms.FolderBrowserDialog browser = new System.Windows.Forms.FolderBrowserDialog();
        string tempPath = "";

        if (browser.ShowDialog() == System.Windows.Forms.DialogResult.OK)
        {
            tempPath = browser.SelectedPath; // prints path
        }

        gallery.Path = tempPath;
    }
}

命令:

class AddGalleryCommand : ICommand
{
    public GalleryViewModel _viewModel { get; set; }

    public AddGalleryCommand(GalleryViewModel ViewModel)
    {
        this._viewModel = ViewModel;
    }

    public bool CanExecute(object parameter)
    {
        /*if (parameter == null)
            return false;*/
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        this._viewModel.AddGalleryClick(parameter as Gallery);
    }
}

class DeleteGalleryCommand : ICommand
{
    public GalleryViewModel _viewModel { get; set; }

    public DeleteGalleryCommand(GalleryViewModel ViewModel)
    {
        this._viewModel = ViewModel;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        this._viewModel.DeleteGalleryClick();
    }
}

class ShowFolderDialog : ICommand
{
    public GalleryViewModel _viewModel { get; set; }

    public ShowFolderDialog(GalleryViewModel ViewModel)
    {
        this._viewModel = ViewModel;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        this._viewModel.ShowFolderDialogClick(parameter as Gallery);
    }
}

感谢您到目前为止阅读的时间,我将不胜感激我将获得的每一个建议。

【问题讨论】:

  • 如果您的语法特别失败:public ICommand DeleteCommand =&gt; new SimpleCommand(DeleteProject);,请尝试public ICommand DeleteCommand {get {return new SimpleCommand(DeleteProject);}} 如果可行,则说明您没有运行 c#6
  • 您的问题到底是什么?你的代码没有编译?命令绑定不起作用?
  • 不清楚你在问什么。你实际上得到了什么错误信息?说程序语句是“红色下划线”是完全没用的;它什么也没告诉我们。您是否只是未能为SimpleCommand 提供实现?您在上面发布的代码中没有任何内容。有无数可重用ICommand 实现的示例,但它们并没有神奇地出现。您必须将它们包含在您的项目中,或者通过引用包含它们的库,或者自己编写它们(不难)
  • 抱歉,不清楚。我没有发布带有此错误的代码,我有另一个项目。我有错误“;预期”。 zaitsman 的回答是正确的。所以这是问题,因为我使用的是 Visual Studio 2013?
  • 代码审查访问Code Review Stack Exchange;)

标签: c# wpf mvvm viewmodel


【解决方案1】:

有一些框架/库可以帮助简化命令绑定。例如 MVVMLight 具有 RelayCommand 的通用实现,只需要您创建属性并为其执行分配方法名称。

Here 是如何使用 Mvvmlight 中继命令的示例。

【讨论】:

  • 谢谢。所以我必须创建 Mvvmlight 项目才能使用 RelayCommand?当我省略命令时,我是否正确地使用 mvvm?
  • 如果您已经有自己的 RelayCommands 实现,您可以使用它。但是,如果您不想经历创建自己的 RelayCommand 实现的麻烦,您可以通过 Nuget(或手动)导入 Mvvmlight 并使用它们的实现。由 MvvmLight 开发的 RelayCommand 将确保您遵循 Mvvm 设计模式。
【解决方案2】:

您可以通过使用单个 DelegateCommand 实现而不是单独的 ICommand 类来做到这一点。

public class DelegateCommand : ICommand
{
    private readonly Predicate<object> _canExecute;
    private readonly Action<object> _execute;

    public event EventHandler CanExecuteChanged;

    public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public DelegateCommand(Action<object> execute) : this(execute, null) { }

    public virtual bool CanExecute(object parameter)
    {
        if (_canExecute == null)
        {
            return true;
        }

        return _canExecute(parameter);
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

有两个重载的构造函数,一个只接受要执行的方法,一个接受方法和PredicateCanExecute

用法:

public class ViewModel
{
    public ICommand DeleteProjectCommand => new DelegateCommand(DeleteProject);

    private void DeleteProject(object parameter)
    {
    }
}

关于 MVVM 的进一步简化,实现属性更改通知功能的一种方法是通过以下方式:

public abstract class ObservableObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    internal void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

然后在 ViewModel 中:

public class ViewModel : ObservableObject
{
    private object _myProperty;
    public object MyProperty
    {
        get { return _myProperty; }
        set
        {
            if (_myProperty != value)
            {
                _myProperty = value;
                NotifyPropertyChanged();
            }
        }
    }

    private object _anotherProperty;
    public object AnotherProperty
    {
        get { return _anotherProperty; }
        set
        {
            if (_anotherProperty != value)
            {
                _anotherProperty = value;
                NotifyPropertyChanged();
                NotifyPropertyChanged("MyProperty");
            }
        }
    }
}

请注意,从属性的 setter 中引发 NotifyPropertyChanged 时,您不需要提供该属性的名称(感谢 [CallerMemberName]),尽管它仍然是一个选项,例如,AnotherProperty raises更改两个属性的通知。

澄清

DelegateCommand 将适用于您的所有示例。你传递给它的方法应该有一个签名:

void MethodName(object parameter)

这与ICommandExecute 方法的签名相匹配。参数类型是object,所以它接受任何东西,并且在你的方法中你可以将它转换为你实际传递给它的任何对象,例如:

private void AddGallery(object parameter)
{
    Gallery gallery = (Gallery)parameter;

    ...
}

如果你没有设置CommandParameter,那么null将被传递,所以对于你的另一个例子,你仍然可以使用相同的签名,你只是不会使用参数:

private void DeleteGallery(object parameter)
{
    ...
}

因此,您可以使用DelegateCommand 来完成上述所有操作。

CanAddGallery 实现

以下应该为如何实现这一点提供了一个很好的模型(我发明了两个属性,Property1Property2,来表示您的 TextBox 值):

public class Gallery : ObservableObject
{
    private string _property1;
    public Gallery Property1
    {
        get { return _property1; }
        set
        {
            if (_property1 != value)
            {
                _property1 = value;
                NotifyPropertyChanged();
            }
        }
    }

    private Gallery _property2;
    public Gallery Property2
    {
        get { return _property2; }
        set
        {
            if (_property2 != value)
            {
                _property2 = value;
                NotifyPropertyChanged();
            }
        }
    }

    public Gallery() { }
}

public class AddGalleryViewModel : ObservableObject
{
    private Gallery _galleryToAdd;
    public Gallery GalleryToAdd
    {
        get { return _galleryToAdd; }
        set
        {
            if (_galleryToAdd != value)
            {
                _galleryToAdd = value;
                NotifyPropertyChanged();
            }
        }
    }

    public DelegateCommand AddGalleryCommand { get; set; }

    public AddGalleryViewModel()
    {
        AddGalleryCommand = new DelegateCommand(AddGallery, CanAddGallery)

        GalleryToAdd = new Gallery();
        GalleryToAdd.PropertyChanged += GalleryToAdd_PropertyChanged
    }

    private void AddGallery(object parameter)
    {
        Gallery gallery = (Gallery)parameter;

        ...
    }

    private bool CanAddGallery(object parameter)
    {
        Gallery gallery = (Gallery)parameter;

        if (string.IsNullOrEmpty(gallery.Property1) || string.IsNullOrEmpty(gallery.Property2))
        {
            return false;
        }

        return true;
    }

    private void GalleryToAdd_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Property1" || e.PropertyName == "Property2")
        {
            AddGalleryCommand.RaiseCanExecuteChanged();
        }
    }
}

关于以下实现的说明:

public DelegateCommand AddGalleryCommand => new DelegateCommand(AddGallery, CanAddGallery);

我发现当我使用这个方法时,DelegateCommand 上的CanExecuteChanged EventHandler 总是null,所以这个事件永远不会触发。如果CanExecute 开头是false,则该按钮将始终被禁用-如果它开头为true,我仍然可以在命令执行与否方面获得准确的功能,但该按钮将始终启用。因此,我更喜欢上面例子中的方法,即:

public DelegateCommand AddGalleryCommand { get; set; }

public AddGalleryViewModel()
{
    AddGalleryCommand = new DelegateCommand(AddGallery, CanAddGallery)

    ...
}

DelegateCommand 专业化

以下类允许您为命令参数指定类型:

public class DelegateCommand<T> : ICommand
{
    private readonly Predicate<T> _canExecute;
    private readonly Action<T> _execute;

    public event EventHandler CanExecuteChanged;

    public DelegateCommand(Action<T> execute, Predicate<T> canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public DelegateCommand(Action<T> execute) : this(execute, null) { }

    public virtual bool CanExecute(object parameter)
    {
        if (_canExecute == null)
        {
            return true;
        }

        return _canExecute((T)parameter);
    }

    public void Execute(object parameter)
    {
        _execute((T)parameter);
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

用法:

public DelegateCommand<Gallery> AddGalleryCommand { get; set; }

public AddGalleryViewModel()
{
    AddGalleryCommand = new DelegateCommand<Gallery>(AddGallery, CanAddGallery)
}

private void AddGallery(Gallery gallery)
{
    ...
}

private bool CanAddGallery(Gallery gallery)
{
    ...
}

以下允许您指定无参数方法:

public delegate void ParameterlessAction();
public delegate bool ParameterlessPredicate();

public class InternalDelegateCommand : ICommand
{
    private readonly ParameterlessPredicate _canExecute;
    private readonly ParameterlessAction _execute;

    public event EventHandler CanExecuteChanged;

    public InternalDelegateCommand(ParameterlessAction execute) : this(execute, null) { }

    public InternalDelegateCommand(ParameterlessAction execute, ParameterlessPredicate canExecute)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter)
    {
        if (_canExecute == null)
        {
            return true;
        }

        return _canExecute();
    }

    public void Execute(object parameter)
    {
        _execute();
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }
}

用法:

public InternalDelegateCommand CreateGalleryCommand { get; set; }

public CreateGalleryViewModel()
{
    CreateGalleryCommand = new InternalDelegateCommand(CreateGallery)
}

private void CreateGallery()
{
    Gallery gallery = new Gallery();

    ...
}

【讨论】:

  • 我发送的代码编译没有问题。我发布了它,因为如果我以正确的方式接近 mvvm,我没有人可以检查它。我正在寻找解决方案如何避免将每个命令放在单独的类中。
  • 我仍然认为,我不明白如何使用它。我得到的结论将基于我上面发布的代码。所以我确实需要我的三个功能的命令。其中两个(AddGallery、ShowFolderDialog)需要参数为 Gallery 的命令。所以我可以将这两者合并在一起,因为它们确实需要相同的东西,我会按照你提到的那样称呼它们。第二个将用于 DeleteGallery,因为它不需要任何参数。是这样吗,还是我完全出局了。或者有什么方法可以让所有东西都有一个 DelegateCommand(带参数和不带参数)
  • 我在回答中添加了一些说明。
  • 好的,最后一件事,如何在 ViewModel 构造函数中调用它? AddGalleryCommand = new DelegateCommand(AddGallery); 像这样它不起作用。 编辑: 我忘记在 xaml 中更改路径,这就是我在寻找一些构造函数的原因。它现在正在工作。
  • 能否请您为public ICommand AddGalleryCommand =&gt; new DelegateCommand(AddGallery, CanAddGallery); 实现CanAddGallery 我正在尝试这样做,如果画廊的属性之一为空或空字符串,则该按钮未激活。在文本框中包含某些内容后,它将成为活动按钮。感谢您到目前为止的回复:)
【解决方案3】:

好的,我已尝试尽可能简化它。 我正在使用你的 ObservableObjectDelegateCommand&lt;T&gt;

问题是NullReferenceException 在运行后的CanAddGallery。没有弹出窗口。我试图通过添加if (parameter == null) return false 来解决它。那只会禁用按钮。我在想是否有必要禁用按钮。如果从用户的角度来看禁用按钮不是更好,那么在文本框下显示“必须填写”(或弹出消息)的红色文本,当没有通过按钮发送参数时会出现单击单击。

xaml:

<StackPanel DataContext="{Binding Source={StaticResource gallery}}" Margin="10">
    <ListView DataContext="{Binding Source={StaticResource viewModel}}" 
              SelectedItem="{Binding SelectedGallery}"
              ItemsSource="{Binding GalleryList}"
              Height="150">

        <ListView.View>
            <GridView>
                <GridViewColumn Header="Name" Width="100" DisplayMemberBinding="{Binding Name}"/>
                <GridViewColumn Header="Path" Width="100" DisplayMemberBinding="{Binding Path}"/>
            </GridView>
        </ListView.View>
    </ListView>

    <TextBlock Text="Name" Margin="0, 10, 0, 5"/>
    <TextBox Text="{Binding Name}" />
    <TextBlock Text="Path" Margin="0, 10, 0, 5" />
    <TextBox Text="{Binding Path}" Grid.Column="0"/>
    <Button DataContext="{Binding Source={StaticResource viewModel}}"
            Command="{Binding Path=AddGalleryCommand}" 
            CommandParameter="{Binding Path=GalleryToAdd}" Content="Add"/>
</StackPanel>

型号:

class Gallery : ObservableObject
{
    private string _name;
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            _name = value;
            NotifyPropertyChanged();
        }
    }


    private string _path;

    public string Path
    {
        get
        {
            return _path;
        }
        set
        {
            _path = value;
            NotifyPropertyChanged();
        }
    }
}

视图模型:

class GalleryViewModel : ObservableObject
{
    private ObservableCollection<Gallery> _galleryList;

    public ObservableCollection<Gallery> GalleryList
    {
        get { return _galleryList; }
        set
        {
            _galleryList = value;
            NotifyPropertyChanged();
        }
    }

    private Gallery _galleryToAdd;
    public Gallery GalleryToAdd
    {
        get { return _galleryToAdd; }
        set
        {
            if (_galleryToAdd != value)
            {
                _galleryToAdd = value;
                NotifyPropertyChanged();
            }
        }
    }

    public DelegateCommand<Gallery> AddGalleryCommand { get; set; }

    public GalleryViewModel()
    {
        GalleryList = new ObservableCollection<Gallery>();
        AddGalleryCommand = new DelegateCommand<Gallery>(AddGallery, CanAddGallery);
        GalleryToAdd = new Gallery();
        GalleryToAdd.PropertyChanged += GalleryToAdd_PropertyChanged;
    }

    private void AddGallery(object parameter)
    {
        Gallery gallery = (Gallery)parameter;

        Gallery g = new Gallery();
        g.Name = gallery.Name;
        g.Path = gallery.Path;
        GalleryList.Add(g);
    }

    private bool CanAddGallery(object parameter)
    {
        Gallery gallery = (Gallery)parameter;

        if (string.IsNullOrEmpty(gallery.Name) || string.IsNullOrEmpty(gallery.Path))
        {
            return false;
        }

        return true;
    }

    private void GalleryToAdd_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Name" || e.PropertyName == "Path")
        {
            AddGalleryCommand.RaiseCanExecuteChanged();
        }
    }
}

【讨论】:

  • 好吧,这里的问题是,如果你使用DelegateCommand&lt;Gallery&gt;,你需要传递接受Gallery而不是object的方法,所以private void AddGallery(Gallery gallery)private bool CanAddGallery(Gallery gallery),例如.
  • 另外,如果您使用这种方法,AddGallery 可以简化为:private void AddGallery(Gallery gallery) { GalleryList.Add(gallery); }
  • 好的,我不得不进一步研究这个问题,事实证明NullReferenceException 的解决方案是将Command Parameter 放在XAML 中的Command 之前!有关更多信息,请参阅@TravisWeber 的答案:stackoverflow.com/questions/335849/…
  • 另外,另一种选择是使用InternalDelegateCommand(无参数)并在命令方法中直接引用GalleryToAdd - 这似乎是目前更简单的解决方案! :)
  • 好的,我已经尝试了第一个解决方案。不再有NullReferenceException,但该按钮从未启用。我在GalleryToAdd setter 中设置了断点,它只有在应用程序启动时才会出现。当我更改文本框时,它不会执行更改。问题应该出在 xaml + 模型中,因为数据流是从视图到模型的。当您单击添加时,应该添加模型,但该模型不在视图模型中,没有对由文本框制作的当前对象的引用。
猜你喜欢
  • 1970-01-01
  • 2012-01-29
  • 2012-10-06
  • 1970-01-01
  • 2020-06-08
  • 1970-01-01
  • 1970-01-01
  • 2018-05-19
相关资源
最近更新 更多