0

Trying to get to grips with MVVM in WPF c#.

I am a slow learner...

I have my MainWindow.xaml.

This is the markup in question:

<Viewbox x:Name="vbxucProductCostMenu" Stretch="{Binding Stretch}" StretchDirection="Both">
     //a user control
</Viewbox>

<Button Command="{Binding MagnifyMinimiseCommand}" CommandParameter="UniformToFill">
    <Image Source="Images/button_plus_green.png"/>
</Button>

Part of my MainWindow.cs

 public MainWindow()
 {
     InitializeComponent();
     this.DataContext = new MagnifyMinimise();
 }

My Viewmodel?

public class MagnifyMinimise : INotifyPropertyChanged
{
    public MagnifyMinimise()
    {
        Minimise();
    }

    MagnifyMinimiseCommand _magnifyMinimiseCommand = new MagnifyMinimiseCommand();
    public MagnifyMinimiseCommand MagnifyMinimiseCommand
    {
        get { return _magnifyMinimiseCommand; }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void Magnify()
    {
        Stretch = "UniformToFill";
    }
    public void Minimise()
    {
        Stretch = "None";
    }

    public string Stretch { get; set; }

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

my 'ICommand' class:

public class MagnifyMinimiseCommand : ICommand
{
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        //how do I set the property of stretch here!!
    }
}

When I run this it starts up minimized which is good.

I then want to 'maximize' the viewbox when the user clicks that button.

By setting the breakpoint in the 'Execute' method i can see that it is being invoked and the 'parameter' is set to 'UniformToFill'.

But how do I get the Stretch property to 'read' that?

ADDITONAL:

I have changed it all to this (which does not work):

public class MagnifyMinimise : INotifyPropertyChanged {

    private ActionCommand<string> _magnifyMinimiseCommand;
    public MagnifyMinimise()
    {
        Minimise();
        _magnifyMinimiseCommand = new ActionCommand<string>(Magnify);
    }

    private void Magnify(string stretch)
    {
        // now the viewmodel handles it instead of the action
        Stretch = stretch;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void Magnify()
    {
        Stretch = "UniformToFill";
    }
    public void Minimise()
    {
        Stretch = "None";
    }

    public string Stretch { get; set; }

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

public class ActionCommand<T> : ICommand where T : class
{
    private readonly Action<T> mAction;

    public ActionCommand(Action<T> action)
    {
        mAction = action;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        mAction(parameter as T);
    }

    public event EventHandler CanExecuteChanged;
}

<Button Command="{Binding ActionCommand}"  CommandParameter="UniformToFill">
    <Image Source="Images/button_plus_green.png" />
</Button>
Andrew Simpson
  • 6,883
  • 11
  • 79
  • 179
  • Just pass some `Action` into your `MagnifyMinimiseCommand` constructor that will be able to set the Stretch property while being invoked in the `Execute` method. Or better use something like [RelayCommand](http://stackoverflow.com/questions/22285866/why-relaycommand). – Eugene Podskal Mar 06 '16 at 09:55
  • @EugenePodskal thanks for link. Looking at it now. Would you illustrate what you mean by Action like i said i am a bit a slow :) – Andrew Simpson Mar 06 '16 at 09:56
  • 3
    That's a personal view of MVVM, but I wouldn't expose this "Stretch" property. The VM should be agnostic of the view, but by exposing this Stretch, the VM is forcing the view to implement the magnifying in a specific way. Ideally, the VM should expose a bool `IsMagnifying` and the view should convert it to Stretch using either a `Trigger` or a `IValueConverter`. I'm aware it doesn't solve your problem, it's just a general remark – Kevin Gosse Mar 06 '16 at 10:19
  • @KooKiz thanks. There is a lot to learn for me in all this. I want to do things as simple as possible and I understand your comment on this and I will adopt because it makes sense. Thanks – Andrew Simpson Mar 06 '16 at 10:21
  • @KooKiz That's correct, but there shouldn't even be a IsMagnifying property - there is no need for the viewmodel to track anything like that. Being magnified or minified is purely a concern of the UI (View) not the VM. – slugster Mar 06 '16 at 10:32
  • @KooKiz OK. well I more confused than ever now. I thought I needed to use iCommand instead of the 'click' event with code behind to follow best practice in WPF and to use MVVM. This is the issue of wpf for me. this is why I am having difficulty learning this all. But thanks :) – Andrew Simpson Mar 06 '16 at 10:36
  • @slugster Well it depends on the app. If it's just to help reading, like as an accessibility tool, then indeed it's the realm of the view. On the other hand, the magnifying tool in Photoshop would be a business feature, and therefore would belong to the viewmodel. That said, the implementation in the question (the magnifying done by just using the Stretch property) tends to prove you're right – Kevin Gosse Mar 06 '16 at 10:51
  • @KooKiz understood I probably have chosen a very weak example to learn this stuff – Andrew Simpson Mar 06 '16 at 10:52

3 Answers3

1

The easiest way is, like suggested by @Default, to use a RelayCommand. There is one (or an alternative) provided in every major MVVM framework (Prism, MVVM Light, Caliburn.Micro, ...).

That said, if you wanted to solve the issue with your vanilla implementation of a command, you'd just have to pass a reference to the viewmodel in the constructor:

public class MagnifyMinimiseCommand : ICommand
{
    public MagnifyMinimiseCommand(MagnifyMinimise  viewModel)
    {
        this.ViewModel = viewModel;
    }

    protected MagnifyMinimise ViewModel { get; }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        this.ViewModel.IsMagnifying = "...";
    }
}
Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • Hi,, thanks for your answer. I will give this a go. I am also wanting to do things the correct way (best practice) so I will look at everyone's answers – Andrew Simpson Mar 06 '16 at 10:24
  • I have implemented what you suggested above. But get an error (obviously). It is here: MagnifyMinimiseCommand _magnifyMinimiseCommand = new MagnifyMinimiseCommand(); (what do I pass in?) – Andrew Simpson Mar 06 '16 at 10:27
  • -1 for suggesting to put View related stuff into the ViewModel. There is absolutely no reason why this cannot be handled exclusively in the View. – slugster Mar 06 '16 at 10:28
  • 1
    I thought of this solution as well, but you have just created so much code in order to change one type of property for one type of class. Next time you want to change another property - would the solution be to create yet another command? In the long run, that's not maintainable. – default Mar 06 '16 at 10:36
  • This is so easily done with a simple `DelegateCommand` implemented in the code behind of the view. If you modify your code to something a little more MVVM I'll remove my down vote. – slugster Mar 06 '16 at 10:41
  • @slugster View stuff in the viewmodel? Where do you see that? – Kevin Gosse Mar 06 '16 at 10:45
  • @Default I agree that the RelayCommand is the best solution, that's the first thing I said in my answer. This was just for documentation purpose: "if, for some reason, you want to implement the command yourself, here's how it's done" – Kevin Gosse Mar 06 '16 at 10:46
  • Guess I'm gonna highlight the part where I explain the purpose of this answer, since everybody is missing it :P – Kevin Gosse Mar 06 '16 at 10:47
  • @KooKiz I understood what you meant :) You would not normally do this - I understand. You just gave an answer to my context which is appreciated – Andrew Simpson Mar 06 '16 at 10:50
  • You inject a VM in the ctor of the command (that in itself is okay), then you set a View related property on that VM in the `Execute()` of the command. Note that I'm saying this is an immediate view related property or setting because the OP hasn't said otherwise. Keep in mind that this OP has stated a lack of knowledge, so let's set him on the right track rather than just enabling bad code :) – slugster Mar 06 '16 at 10:51
  • @slugster Oh I see your point. I was just putting "Stretch" to stick with the question and being easier to understand. Edited. – Kevin Gosse Mar 06 '16 at 10:53
  • @all I am still unsure what to implement. i have read all that everyone has suggested. If we assume that although this is a weak example of mine but let it belong in the MVVM (so I can use for other cases) and bearing in mind I want code to be simple and testable then what direction should i go? – Andrew Simpson Mar 06 '16 at 11:04
0

You need to invoke PropertyChanged for Stretch. That's how i would do it:

private string _stretch;
public string Stretch 
{ 
   get { return _stretch; } 
   set {_stretch = value; OnPropertyChanged("Stretch"); }
}

Also you might want to consider using RelayCommand or DelegateCommand

Another sidenote: In MVVM try not to write any code in the view's code behind. Use App.xaml.cs for setting the DataContext of the view.

EDIT: To answer your question, i would create a DelegateCommand class like this:

class DelegateCommand : ICommand
{
    private readonly Action<Object> _execute;
    private readonly Func<Object, Boolean> _canExecute;

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

    public DelegateCommand(Func<Object, Boolean> canExecute, Action<Object> execute)
    {
        if (execute == null)
        {
            throw new ArgumentNullException("execute");
        }

        _execute = execute;
        _canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged;

    public Boolean CanExecute(Object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public void Execute(Object parameter)
    {
        if (!CanExecute(parameter))
        {
            throw new InvalidOperationException("Command execution is disabled.");
        }
        _execute(parameter);
    }

    public void OnCanExecuteChanged()
    {
        if (CanExecuteChanged != null)
            CanExecuteChanged(this, EventArgs.Empty);
    }
}

and use it like this in your viewmodel:

public DelegateCommand MagnifyMinimiseCommand { get; private set; }
.....
MagnifyMinimiseCommand = new DelegateCommand(param => { Stretch = UniformToFill; });

then

  <Button Command="{Binding MagnifyMinimiseCommand}">
      <Image Source="Images/button_plus_green.png"/>
  </Button>
  • Thanks for your answer and the hint for moving the DataContext to app. Whilst I understand the code you have supplied what do I put in the Execute method? – Andrew Simpson Mar 06 '16 at 10:03
  • and in the code public void Execute(object parameter) { ((Viewbox)parameter).Stretch = Stretch.UniformToFill; } – Norbert Kovacs Mar 06 '16 at 10:13
  • have not checked this out yet but I also have another button to minimise. The code above shows just hot to maxmise? – Andrew Simpson Mar 06 '16 at 10:31
  • *"In MVVM try not to write any code in the view's code behind"* - why not? If it's view related code then it belongs in the view, and if it's code rather than XAML then it belongs in the code behind. – slugster Mar 06 '16 at 10:35
  • @slugster so are you suggesting keep my click event for such ui behavior matters like this? – Andrew Simpson Mar 06 '16 at 10:37
  • http://stackoverflow.com/questions/6421372/why-to-avoid-the-codebehind-in-wpf-mvvm-pattern – Norbert Kovacs Mar 06 '16 at 10:37
  • Note that `App.xaml.cs` is also a code-behind file. @AndrewSimpson no, because since your click event interacts with the data layer, the viewmodel is most likely the place to handle it. I believe what he's saying is that you shouldn't blindly go be the rule that "never put anything in the views code-behind". It's a very limiting rule. – default Mar 06 '16 at 10:38
  • @Default I would question whether Andrew needs to have any interaction with the VM - it appears at this stage that he has simply put stuff there due to lack of knowledge. If the UI state or interaction properties are moved to the view then a simple click event handler could be used. Or a command. There are several ways to skin this cat, but putting properties in the right place is a good way to start :) – slugster Mar 06 '16 at 10:48
  • @slugster I appreciate this. – Andrew Simpson Mar 06 '16 at 10:55
0

Instead of using such a specific type of Command, you can create a more generic command and allow the viewmodel to handle the action itself. So create a generic type of ICommand:

public class ActionCommand<T> : ICommand where T : class
{
    private readonly Action<T> mAction;

    public ActionCommand(Action<T> action)
    {
        mAction = action;
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        mAction(parameter as T);
    }

    public event EventHandler CanExecuteChanged;
}

and then create it like this:

private ActionCommand<string> _magnifyMinimiseCommand;
public MagnifyMinimise()
{
    _magnifyMinimiseCommand = new ActionCommand<string>(Magnify);
    ....
}

private void Magnify(string stretch)
{
    // now the viewmodel handles it instead of the action
    Stretch = stretch;
}

Also, as a common practice I usually expose the properties to the View as it's interfaces, so the MagnifyMinimiseCommand would for instance be an ICommand instead (you can still use the field to access the ActionCommands stuff).

default
  • 11,485
  • 9
  • 66
  • 102
  • I have implemented this but it errors that Stretch does not exist. Also, is this a MVVM pattern? – Andrew Simpson Mar 06 '16 at 10:33
  • Sorry, the command should be in your MagnifyMinimise class, not in your view. I read that wrong. And your MagnifyMinimise has a Stretch property, correct? – default Mar 06 '16 at 10:41
  • yes that is correct (thank God! :)) what do i put in my markup for the binding then as my OnPropertyChanged is no longer being invoked – Andrew Simpson Mar 06 '16 at 10:48
  • are you suggesting that I do not use a binding element this? – Andrew Simpson Mar 06 '16 at 10:56
  • please supply the binding element. I cannot get this to work. Sorry for being so thick – Andrew Simpson Mar 06 '16 at 11:09
  • not sure what you mean. Why are you not invoking OnPropertyChanged? In order for the view to get updates for anything from the viewmodel you need it to fire a PropertyChanged event. – default Mar 06 '16 at 11:12
  • But that is what I am saying. Before in my weak question I was invoking this method but replacing my classes with your classes it no longer does – Andrew Simpson Mar 06 '16 at 11:13
  • well, again, change the `Stretch` setter to `set{ _stretch = value; OnPropertyChanged("Stretch"); }`. Again, you have to call OnPropertyChanged in order for the view to understand that a property has changed. Also, why did you change the binding to `ActionCommand`? Unless there's a property called `public .. ActionCommand { get .. }` that won't work. I never told you to remove all of your code, so please don't blame me for that. I showed you how to implement an `ICommand` that accepts an `Action` which you can then use in your ViewModel. – default Mar 06 '16 at 11:27
  • I am not blaming you. I changed to ActionCommand in depseration as I could not get it to work – Andrew Simpson Mar 06 '16 at 11:29
  • I am trying to get 'OnPropertyChanged' invoked. I am telling you it is no longer being invoked – Andrew Simpson Mar 06 '16 at 11:30
  • it was calling 'MagnifyMinimiseCommand' before but that no longer exists as you are telling me to use 'ActionCommand' class instead. I have been given several answerr to my questions. everyone tells me different things. i cannot met the leaps of logic from your answer that is require because this is all new to me. Never mind your time is appreciated – Andrew Simpson Mar 06 '16 at 11:32