用于创建某些模型的用户控件

User controls for creating certain models

我是 WPF 的新手,我发现为我的案例创建一个自定义组件是最好的,所以如果我一开始说错了请告诉我。这个想法的目的是根据需要在其他场景中复用。

Model:

public class FooModel
{
    public string Whatever { get; set; }
}

ViewModel:

public class FooViewModel
{
    public FooModel Foo { get; set; }

    public ICommand CreateCommand { get; set; } = new AnotherCommandImplementation<FooModel>(model =>
    {
        // model is null! :(
    });
}

UserControl:

<UserControl>
    <UserControl.DataContext>
        <local:FooViewModel />
    </UserControl.DataContext>
    <StackPanel Orientation="Horizontal">
        <TextBox Text="{Binding Foo.Whatever}" Height="23" Width="120"/>
        <Button CommandParameter="{Binding Foo}" Command="{Binding CreateCommand}" Width="80" Content="Create"/>
    </StackPanel>
</UserControl>

为什么 Foo 为空,我该如何解决?

更新

根据要求,这是当前的 DataTemplate 技术尝试:

App.xaml:

<Application>
    <Application.Resources>
        <DataTemplate DataType="{x:Type vms:KeyboardActionViewModel}">
            <ctrs:KeyboardActionControl />
        </DataTemplate>
    </Application.Resources>
</Application>

Window:

<Window>
    <Window.DataContext>
        <vms:ActionExecutorViewModel />
    </Window.DataContext>
    <StackPanel>
        <CheckBox IsChecked="{Binding Enabled}" Content="Enabled" />
        <UserControl Content="{Binding Action}" />
    </StackPanel>
</Window>

视图模型:

public class ActionExecutorViewModel : ViewModel<ActionExecutor>
{
    private Boolean enabled;
    private ActionViewModel action;

    public ActionExecutorViewModel()
    {
        Action = new KeyboardActionViewModel(); // Test
    }

    public ActionViewModel Action
    {
        get => action;
        set => AssignAndRaiseEventOnPropertyChange(ref action, value);
    }

    public Boolean Enabled
    {
        get => enabled;
        set => AssignAndRaiseEventOnPropertyChange(ref enabled, value);
    }

    public override ActionExecutor BuildModel()
    {
        var executor = new ActionExecutor();

        executor.Action = action.BuildModel();

        return executor;
    }
}

KeyboardActionControl:

<UserControl>
    <Label Background="Aqua">Asadsadsad</Label>
</UserControl>

ActionViewModel 是一个抽象 class,其中 KeyboardActionViewModel 继承自它。

没有使用非默认值初始化 Foo 的构造函数(null 用于引用类型)。这就是原因。至少,提供这样一个构造函数,或者——更多 WPFic 方式——创建一个 DataContext="{Binding Foo}";这可能就是您想要的,但是您的 XAML 是错误的:您一直在创建 new 实例,而不是使用视图模型的 Foo 实例。

P.S。更重要的是,对于 UserControls,它是暴露一个 DependencyProperty 以获取底层模型的命令;所以它看起来像 <UserControl Model="{Binding Foo}" ... />.

正如 Sereja 指出的那样,您的近端问题是 Foo 为空。你从未创造过它,所以它不存在。它可能应该由 FooViewModel 实例化,但 FooViewModel 的创建者也有可能创建 Foo。在不知道语义的情况下,我无法确定。视图绝对不应该负责创建任何一个。

但是您所做的事情中存在错误的假设。让我们纠正这些,让你走上正确的轨道。

ViewModelBase 实施 INotifyPropertyChanged。例子比比皆是。下面的视图 XAML 片段是部分的: UI 的一些片段没有说明,因为它们不应该造成任何困难。

public class MainViewModel : ViewModelBase
{
    public ActionExecutorCollectionViewModel ActionExecutors { /* INPC stuff */ }
        //  ViewModels create their own children. 
        = new ActionExecutorCollectionViewModel();
}

public class ActionExecutorCollectionViewModel : ViewModelBase
{
    public ObservableCollection<ActionExecutor> Items { /* INPC stuff */ }
    public ActionExecutor NewActionExecutor { /* INPC stuff */ }

    // Create new ActionExecutor and assign to NewActionExecutor 
    public ICommand CreateActionExecutor { /* ... */ }

    // Add NewActionExecutor to Items and set NewActionExecutor to null 
    public ICommand SaveActionExecutor { /* ... */ }
}

为以上各项编写一个隐式数据模板。在MainViewModel的DataTemplate中,有这样的东西:

<ContentControl Content="{Binding ActionExecutors}" />

显示带有隐式 DataTemplate 的 ActionExecutorsViewModel,其中包含如下内容,以及其他内容:

<Button
    Command="{Binding CreateActionExecutor}"
    Content="Create"
    />
<Button
    Command="{Binding SaveActionExecutor}"
    Content="Save"
    />
<ContentControl
    Content="{Binding NewActionExecutor}"
    />

ActionExecutor 需要某种粗糙的 class 工厂来创建自己的 Action。您现在有两种操作类型。我建议不要在试图编写一个完美的架构以在将来添加新架构时发疯。相反,我建议给 ActionExecutor 一个 public 只读的动作类型选项集合,可能是来自枚举的值:public ActionType { Mouse, Keyboard }public ActionType ActionType 属性。当 ActionType 改变时,创建一个新类型的新动作并将其分配给 Action 属性。 ActionType 的 setter 应该调用执行该操作的受保护方法。对此还有其他更聪明的选择,但上述设计是合理可维护的,并且在数以千计的生产应用程序中运行良好。

在 ActionExecutor 的隐式 DataTemplate 中,您将有一个组合框,它允许用户 select 来自 ActionTypes 集合的一种动作。它的 SelectedItem 属性 绑定到 ActionType。这就是创建动作的方式。

ActionExecutor 的 DataTemplate 包含如下内容:

<CheckBox Content="Enabled" IsChecked="{Binding Enabled}" />
<ComboBox ItemsSource="{Binding ActionTypes}" SelectedItem="{Binding ActionType}" />
<ContentControl Content="{Binding Action}" />
  1. MainViewModel 下面的所有视图模型都是由它们的直接父视图模型创建的,never never never never ever ever by a view。将视图模型 "tree" 视为应用程序的骨架或框架。视图只是根据需要显示它的一部分。 视图模型需要相互通信;观看次数没有。他们只是在他们的视图模型中反映和激发状态变化。 window 可以在其构造函数中创建其视图模型,或在 XAML 中创建为 <Window.DataContext><local:MainViewModel /></Window.DataContext>。两者都可以,但是在构造函数中执行此操作允许您调用具有参数的构造函数。

  2. 因此,除了那个例外,UserControl 总是 从上下文中获取它的 DataContext,永不 通过创建它。这是一个实际问题,而不是意识形态问题:它使编写和维护应用程序比其他方法容易得多。当你遵循这条规则时,许多棘手的问题都会迎刃而解。设计良好的 WPF 应用程序中的 UserControl 很少定义依赖属性。 UserControl 的目的是显示视图模型。其他类型的控件将定义大量、丰富、闪闪发光的依赖属性。不是用户控件。

  3. 您可以编写UserControls并将它们放在DataTemplates中,或者只编写DataTemplates。我相信编写 UserControls 是个好主意。包含 UserControl 的 DataTemplate 看起来完全像这样:

    <DataTemplate DataType="{x:Type ActionExecutor}">
        <local:ActionExecutorUserControl />
    </DataTemplate>
    
  4. DataContext="{Binding SomeProperty}" 本质上总是错误的。这是一种“代码味道”,表明有人还没有很好地理解 XAML。

如果上面的某些部分对您来说没有意义,我很乐意帮助您填补知识空白。如果您认为它的某些部分与您的要求相冲突,您很可能就错了。但是,您有责任充分理解和整理您自己的要求,并清楚地传达这些要求。

更新

隐式数据模板

隐式数据模板是 1) 定义为可访问 ResourceDictionary 中的资源的数据模板,以及 2) DataType 属性,用于指定要与其一起显示的 classes。

App.xaml

<Application.Resources>
    <DataTemplate DataType="{x:Type ActionExecutorCollectionViewModel}">
        <local:ActionExecutorCollectionUserControl />
    </DataTemplate>
    <DataTemplate DataType="{x:Type ActionExecutor}">
        <local:ActionExecutorUserControl />
    </DataTemplate>
    <DataTemplate DataType="{x:Type MouseAction}">
        <local:MouseActionUserControl />
    </DataTemplate>
    <!-- And so on and so forth. -->
</Application.Resources>

MainWindow.xaml

MainWindow 的 DataContext 是您的 MainViewModel,我已在上面对其进行了部分定义。

<Grid>
    <!-- 
    MainViewModel.ActionExecutors is of type ActionExecutorCollectionViewModel.
    If you defined an implicit datatemplate for that class in some ResourceDictionary 
    that's in scope here (e.g., App.xaml), this UserControl will automatically 
    use that datatemplate. 
    -->
    <UserControl Content="{Binding ActionExecutors}" />
</Grid>

ActionExecutorUserControl.xaml

<StackPanel>
    <StackPanel Orientation="Horizontal">
        <Label>Interval</Label>
        <TextBox Text="{Binding Interval}" />
    </StackPanel>
    <CheckBox IsChecked="{Binding Enabled}">Enabled</CheckBox>

    <!-- 
    If you have implicit datatemplates defined for all your action types,
    the framework will automatically give this UserControl the correct template
    for whatever actual type of action the Action property refers to.

    This is where we begin to see the real value of implicit datatemplates. 
    -->
    <UserControl Content="{Binding Action}" />
</StackPanel>