Andrew Simpson
Andrew Simpson

Reputation: 7344

Missing understanding of MVVM

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>

Upvotes: 0

Views: 200

Answers (3)

Kevin Gosse
Kevin Gosse

Reputation: 39027

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 = "...";
    }
}

Upvotes: 1

default
default

Reputation: 11635

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).

Upvotes: 0

Norbert Kovacs
Norbert Kovacs

Reputation: 149

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>

Upvotes: 0

Related Questions