1

Following my previous question, I now can drag an item in a canvas through a behavior.

The problem is that the item new coordinates calculations are done internally in the behavior, and I'm not sure how to send them to the item's ViewModel without writing anything in the view.

My MainViewModel has an Items collection :

public BindableCollection<ItemViewModel> Items { get; set; }

The ItemViewModel looks like this :

[ImplementPropertyChanged]
public class ItemViewModel
{
    private readonly IEventAggregator events;
    private Random r = new Random();

    public ItemViewModel(IEventAggregator events)
    {
        this.events = events;
        this.events.Subscribe(this);

        // default values
        this.BackgroundColor = this.RandomColor();
        this.Width = 100;
        this.Height = 100;
        this.X = 10;
        this.Y = 10;
    }

    public Brush BackgroundColor { get; set; }
    public string Content { get; set; }
    public int Height { get; set; }
    public int Width { get; set; }
    public double X { get; set; }
    public double Y { get; set; }

    /// <summary>
    /// https://stackoverflow.com/a/11282427/6776
    /// </summary>
    private SolidColorBrush RandomColor()
    {
        var properties = typeof(Brushes).GetProperties();
        var count = properties.Count();
        var color = properties.Select(x => new { Property = x, Index = this.r.Next(count) })
                              .OrderBy(x => x.Index)
                              .First();

        return (SolidColorBrush)color.Property.GetValue(color, null);
    }
}

And my DragBehavior, for now, is just a copy/paste of this code snippet.

And finally, my MainView displays the items like this (I'm using Caliburn.Micro to bind the items through x:Name) :

<ItemsControl x:Name="Items">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Left" Value="{Binding Path=X}" />
            <Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
            <Setter Property="Width" Value="{Binding Path=Width}" />
            <Setter Property="Height" Value="{Binding Path=Height}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Background="{Binding Path=BackgroundColor}"
                behaviors:DragBehavior.Drag="True">
                <!-- contents -->
            </Border>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

I was thinking of adding something along the lines of this :

<Border ...
    behaviors:DragBehavior.OnDropped="{Binding Dropped}">

And in the ItemViewModel :

public void Dropped(Point newCoordinates) {
  // change the X and Y of the viewmodel
}

But I have no idea where to start, or even what to call it. Action ? Trigger ? It doesn't seem to relate to what I'm looking for.

The Gong DragDropBehavior library has a system where the item implements an interface and the behavior runs the proper method, but I don't really understand how it works (it's the first time I work with behaviors).

Community
  • 1
  • 1
thomasb
  • 5,816
  • 10
  • 57
  • 92

1 Answers1

0

So far, here is the method I use (inspired from how Gong works).

Basically, the view binds on the viewmodel itself, which should implement an interface. The behavior retrieves this binding, and calls the interface method through the bound element.

I have a GitHub account with this code : https://github.com/cosmo0/DragSnap (not yet up-to-date as of this writing).

The behavior has a new DropHandler property :

    public static readonly DependencyProperty DropHandlerProperty =
        DependencyProperty.RegisterAttached(
            "DropHandler",
            typeof(IDropHandler),
            typeof(DragOnCanvasBehavior),
            new PropertyMetadata(OnDropHandlerChanged));

    private IDropHandler DropHandler { get; set; }

    public static IDropHandler GetDropHandler(UIElement target)
    {
        return (IDropHandler)target.GetValue(DropHandlerProperty);
    }

    public static void SetDropHandler(UIElement target, IDropHandler value)
    {
        target.SetValue(DropHandlerProperty, value);
    }

    private static void OnDropHandlerChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        // Initialize variables and assign handlers
        // see original code in Github linked in original question

        // Additionnaly, initialize the DropHandler :
        var handler = (IDropHandler)(e.NewValue);
        Instance.DropHandler = handler;
    }

    private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        var element = (UIElement)sender;
        element.ReleaseMouseCapture();

        // this is the main change
        if (this.DropHandler != null)
        {
            this.DropHandler.Dropped();
        }
    }

    private void ElementOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
    {
        // TODO : do some calculations

        this.DropHandler.Moved(x, y);
    }

My ItemViewModel implements IDropHandler, and the Dropped and Moved methods :

    public void Dropped()
    {
        this.events.PublishOnUIThread(new ItemDroppedEvent(this.X, this.Y, this.Width, this.Height, this.ID));
    }

    public void Moved(double x, double y)
    {
        this.X = x;
        this.Y = y;
    }

And the view binds on the viewmodel itself :

<Border
    behaviors:DragOnCanvasBehavior.DropHandler="{Binding}">

I still have problems handling the dragged element position, but the actual question (sending a message to the viewmodel from the behavior) is answered.

thomasb
  • 5,816
  • 10
  • 57
  • 92