MVVM: run view-only code after calling command

≡放荡痞女 提交于 2019-12-25 07:59:45

问题


What I have:

  • using MVVM pattern
  • a view written in XAML
  • a command MyCommand in the ViewModel which gets called from several places in the view
  • a method DoSthInView that operates on the view, defined in codebehind

My Goal:

Whenever the command is executed, I want to call DoSthInView, no matter which control executed the command.

Question:

Since in MVVM the ViewModel does not know the View, I cannot call DoSthInView from the ViewModel. So how do call this code?

Own thoughts:

To be less abstract, this is my use case: We have one Button and one TextBox. The command takes the text which is currently in the TextBox and writes it somewhere into the model data. When this writing is done, I want to animate a green checkmark appearing and fading out (this is DoSthInView), so that the user gets a visual confirmation that the data was updated.

There are two ways of running the command:

  1. Click the Button
  2. Press "Enter" while the TextBox is focused

For the Button I know a way to call DoSthInView:

<Button Content="run command" Command="{Binding MyCommand}" Click={Binding DoSthInView}" />

For the TextBox, I have a KeyBinding to take the Enter key:

<TextBox>
    <TextBox.InputBindings>
        <KeyBinding Command="{Binding MyCommand}" Key="Enter" />
    </TextBox.InputBindings>
</TextBox>

But InputBindings seem not to support events, only commands. So here I have no idea how to call DoSthInView.

But even if I found a way to call DoSthInView from within the input binding (analog to the Button), it wouldn't feel right. I am looking for a way to say "whenever MyCommand is executed, run DoSthInView" So that not every caller of MyCommand has to care for it individually, but there is just one place to handle that. Maybe this can be done in the root FrameworkElement?


回答1:


What you are asking for is possible. You need to implement RelayCommand. You can also see my other SO answer where there is an example.

Once you have RelayCommand implemented then you can do the following:

In ViewModel:

public ICommand MyCommand { get; set; }
public MyViewModel()
{
    MyCommand = new RelayCommand(MyCommand_Execute);
}

private void MyCommand_Execute(object sender)
{
    var myView = sender as MyView;

    myView?.DoSthInView();
}

In View:

<TextBox>
    <TextBox.InputBindings>
        <KeyBinding Command="{Binding Path=MyCommand}" CommandParameter="{Binding}" Key="Enter"/>
    </TextBox.InputBindings>
 </TextBox>

While it is not recommended to mix view and viewModel, there can be scenarios where otherwise is not possible. and sometimes it can be requirements. But again this is NOT recommended.




回答2:


While I am still interested in an answer to my original question (calling codebehind-code after a command is executed), Kirenenko's advice helped me to solve my actual problem regarding the animation. This answer doesn't fit the original question any more because there is no codebehind-code (the animation is solely written in XAML, leaving no codebehind-code to execute). I still put it here because it is partially useful for me.

In the ViewModel, I have this:

...
private bool _triggerBool;
public bool TriggerBool
{
    get { return _triggerBool; }
    set
    {
        if (_triggerBool != value)
        {
            _triggerBool = value;
            NotifyPropertyChanged(nameof(TriggerBool));
        }
    }
}
...
public DelegateCommand MyCommand; // consists of MyCommandExecute and MyCommandCanExecute
...
public void MyCommandExecute()
{
    ... // actual command code
    TriggerBool = true;
    TriggerBool = false;
}
...

And here is the animation written in XAML and called by the DataTrigger:

<Image Source="myGreenCheckmark.png" Opacity="0">
    <Image.Style>
        <Style TargetType="Image">
            <Style.Triggers>
                <DataTrigger Binding="{Binding TriggerBool}" Value="True">
                    <DataTrigger.EnterActions>
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation Storyboard.TargetProperty="Opacity"
                                                 From="1" To="0" Duration="0.0:0:0.750"/>
                            </Storyboard>
                        </BeginStoryboard>
                    </DataTrigger.EnterActions>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Image.Style>

Looks really stupid to set TriggerBool to true and then false again, but works...




回答3:


Based on Leonid Malyshev's hint here is a quite clean solution using an event ("clean" regarding seperation of View and ViewModel):

ViewModel code:

public class MyViewModel
    {
    ...
    public event Action MyCommandExecuted;
    public DelegateCommand MyCommand; // consists of MyCommandExecute, MyCommandCanExecute
    ...
    private void MyCommandExecute()
    {
        ... // actual command code
        MyCommandExecuted.Invoke();
    }
    ...
}

View Codebehind:

public partial class MyView : Window
{
    public MyView(MyViewModel vm)
    {
        InitializeComponent();
        DataConext = vm;
        vm.MyCommandExecuted += DoSthInView();
    }
    ...
    private void DoSthInView()
    {
        ...
    }
    ...
}



回答4:


Usually you have a better more MVVM-ish way to these this thing but since I don't know your case I'll assume that there is no other way. So to this you need a dependency property in your view. A Boolean would be great and you handle it's on changed event and run DoSthInView in it. In your view model you set the value of this property and on changed gets called.

I can give you demonstration if you need. Also keep in mind that this is event driven coding which defiles MVVM. Try to use bindings and move your DoSthInView to ViewModel if possible.



来源:https://stackoverflow.com/questions/40081038/mvvm-run-view-only-code-after-calling-command

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!