0

Almost all examples of MVVM are applications that have View that may consist of ListBoxes, TextBoxes, Labels, Buttons, etc. to present and process data. ListBoxes, TextBoxes, and Labels are bound to properties and Buttons are bound to Commands in a ViewModel. Those types of Views are mainly static in nature.

However, there are not any real examples of how to deal with a fluid View, such as a drawing program. When you think about it, a drawing program is just a Canvas. Because there are no Buttons to which to bind nor Listboxes, TextBoxes, or Labels to interact with, how should you interact with the Canvas?

So far, the only way that I have found to interact with the Canvas is to use the Canvas' code-behind to use the Mouse Events to allow you to pass data into the ViewModel through exposed methods such as Add(Point point) which will add a point to an ObservableCollection only to be bound to an ItemsControl that will process the point data based on the ItemTemplate.

CanvasView

<ItemsControl x:Name="itemsControl" ItemsSource="{Binding CanvasObjectModels}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas Width="1000" Height="1000" Background="GhostWhite"
                MouseDown="Canvas_MouseDown"/>-->
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Left" Value="{Binding Origin.X}" />
            <Setter Property="Canvas.Top" Value="{Binding Origin.Y}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <CanvasObject:Node Origin="{Binding Origin}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

IMyCanvas

public interface IMyCanvas
{
    public void AddCanvasObject(Point origin);
}

CanvasView Code-Behind

public partial class CanvasView : UserControl
{
    private IMyCanvas pViewModel;

    public CanvasView()
    {
        InitializeComponent();
        Loaded += InitializeViewModel;
    }

    private void InitializeViewModel(object sender, RoutedEventArgs e)
    {
        if (DataContext is IMyCanvas viewModel)
            pViewModel = viewModel;
        else
            throw new Exception("DataContext does not implement IMyCanvas!");
            
        Loaded -= InitializeViewModel;
    }

    private void Canvas_MouseDown(object sender, MouseButtonEventArgs e)
    {
        var canvas = sender as Canvas;

        if (e.LeftButton == MouseButtonState.Pressed)
        {
            pViewModel.AddCanvasObject(e.GetPosition(canvas));
        }
    }
}

CanvasObjectModel

public class CanvasObjectModel
{
    public Point Origin { get; set; }
}

CanvasViewModel

internal class CanvasViewModel : ViewModelBase, IMyCanvas
{
    public ObservableCollection<CanvasObjectModel> CanvasObjectModels { get; set; }

    public CanvasViewModel()
    {
        CanvasObjectModels = new ObservableCollection<CanvasObjectModel>();
    }

    public void AddCanvasObject(Point origin)
    {
        CanvasObjectModels.Add(new CanvasObjectModel() { Origin = origin});
    }
}

Is there a better way to do this?

  • Do I understand you correctly: the main problem is to bind mouse events to some commands in your ViewModel? – GeorgeKarlinzer Aug 11 '23 at 21:27
  • *"interact with the Canvas is to use the Canvas' code-behind to use the Mouse Events to allow you to pass data into the ViewModel through exposed methods"* - That's absolutely fine. *"When you think about it, a drawing program is just a Canvas"* - That's not quite right. A drawing is a complex object similar to any other object in that it is described by a rendered object based on a data object or data model. –  Aug 11 '23 at 22:43
  • For example a line has the data level attributes like a start and end point. It has a thickness an a color. All the data you need in order to restore the original line e.g. after a restart. Then to that data layer maps the rendering layer that contains an object that the framework is able to render. WPF provides multiple ways on different abstraction levels. –  Aug 11 '23 at 22:44
  • For example a Shape object belongs to the simplest and highest or most abstract level. You have to map the data model of the line to a visual like a Line the same way a collection of data models maps to a ListBox. Everything you can see is rendered. It doesn't matter if it is a ListBox or a Shape. All rendered objects are usually view of an associated date model. The View of a drawing program would usually contain a lot more then simply a Canvas. –  Aug 11 '23 at 22:44
  • I have a sort of drawing app, our map editor. That has an inkcanvas you draw in. Once you finish a stroke there's an event raised. I work with the stroke and translate it into points which are then used in a geometry. Beneath the inkcanvas is an itemscontrol with a canvas as itemspanel. I template out viewmodels containing geometries into things in that canvas. I could dig out some code if it helps but there's quite a lot of it. – Andy Aug 12 '23 at 09:34
  • You can compare it with a RichtTextEditor. You need a control (RichTextBox) which can edit the data and the ViewModel gets notified about the changes. So you need (to build) an editor control for drawing. – Sir Rufo Aug 12 '23 at 09:54
  • I would not try to bind this – Joe Aug 12 '23 at 23:18
  • @Joe, care to elaborate as to why you would not try to bind this? – Experiment-626 Aug 14 '23 at 14:09
  • Oops, sorry. Not only did I somehow not finish that comment. I put it under the wrong post. What I meant was in regards to GeorgeKarlinzer's answer. I meant to say that I would not try to bind drawing to *commands* because of the need to maintain *state* during drawing (is the mouse down, is it captured, etc) and the inherent "click" nature of commands – Joe Aug 14 '23 at 18:29
  • @Joe, do you have a suggestion as to how you would go about doing this? Is there a better way than how I shown in my OP? – Experiment-626 Aug 14 '23 at 18:44
  • I am afraid not. I myself have developed a `MultiSelector` control with a `Canvas` that permits the user to draw, select, drag and resize shapes. Been improving it over the years. While it works fairly well, I find I am still more knowledgeable in how I should *not* implement its features than how I should. – Joe Aug 14 '23 at 19:08

2 Answers2

0

If you want to bind events to VM commands you can use Interactivity:

<Canvas Width="1000" Height="1000" Background="GhostWhite">
  <i:Interaction.Triggers>
    <i:EventTrigger EventName="MouseDown">
      <i:InvokeCommandAction Command="{Binding MyCommand}"/>
    </i:EventTrigger>
  </i:Interaction.Triggers>
<Canvas/>

For more information you can check the question

GeorgeKarlinzer
  • 311
  • 2
  • 9
  • Interactivity looks to be a NuGet package. Is this official code from Microsoft? Also, based on this link [https://www.nuget.org/packages/System.Windows.Interactivity.WPF/](https://www.nuget.org/packages/System.Windows.Interactivity.WPF/) it looks like this is no longer used, and according to this link [github.com/MahApps/MahApps.Metro/issues/3400](https://github.com/MahApps/MahApps.Metro/issues/3400) they look to show to use **Microsoft.Xaml.Behaviors.Wpf** as an alternative, which looks like it is shown in an answer by [Kaloyan Manev](https://stackoverflow.com/users/9665681/kaloyan-manev). – Experiment-626 Aug 14 '23 at 14:16
0

You can use the CallMethodAction behavior from Microsoft.Xaml.Behaviors.Wpf (NuGet Package) to directly call your view model methods from XAML.

I am giving you a button example but you can handle the events of your canvas in the same way

1. Add this namespace to your XAML:

xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

2. Add the CallMethodAction trigger to your button in XAML:

<Button x:Name="button">
  <Behaviors:Interaction.Triggers>
    <Behaviors:EventTrigger EventName="Click" SourceObject="{Binding ElementName=button}">
      <Behaviors:CallMethodAction TargetObject="{Binding}" MethodName="IncrementCount"/>
    </Behaviors:EventTrigger>
  </Behaviors:Interaction.Triggers>
</Button>

3. Place a IncrementCount method in your view model:

public int Count { get; set; }

public void IncrementCount()
{
    Count++;
}

Now you can skip the boilerplate code in the code behind. Also, you can check the XamlBehaviorsWpf wiki here: https://github.com/Microsoft/XamlBehaviorsWpf/wiki There are some other behaviors as well :)

Kaloyan Manev
  • 406
  • 6
  • 20