13

Question is as it sounds. No matter how hard and how often I try to understand WPF, I feel like I'm hitting my head against a wall. I like Winforms, where everything makes sense.

As an example, I'm trying to write a simple app that will allow me to lay out a bunch of 2-D paths (represented by polylines) and drag their vertices around, and have the vertex information synchronised with a presenter (that is, I suppose, a ViewModel)

So the problem is thus:

  • Make the window recognise an instance of IExtendedObjectPresenter as a data source;
  • From a collection of IExtendedObject, draw one polyline for each IExtendedObject;
  • For each vertex in the extended object, represented by the collection IExtendedObject.Points, place a polyline vertex at the specified co-ordinates.

The IDE is giving me no help at all here. None of the many properties available in XAML sound meaningful to me. Because so much seems to be done implicitly, there is no obvious place to just tell the window what to do.

Before I get slated and told to RTFM, I would like to re-emphasise that I have researched the basic concepts of WPF a great many times. I know no more about it than I did when it was first released. It seems totally inpenetrable. Examples given for one type of behaviour are not in any way applicable to an even slightly different kind of behaviour, so you're back to square one. I'm hoping that repetition and targeted exampes might switch a light on in my head at some point.

Tom W
  • 5,108
  • 4
  • 30
  • 52
  • @Tom I would advise trying some things in code rather than in Xaml if Xaml is driving you nuts. – Tim Lloyd Jan 22 '11 at 13:29
  • @Tom I would suggest starting out with a `Canvas` control and play about with programatically adding elements to it and changing their positions. You can get a feel for working with paths, but without the added head-boiler of Xaml to contend with at the same time. – Tim Lloyd Jan 22 '11 at 13:42
  • Whoever votes to close, please explain why. It is a valid question. Thank you. – Tom W Jan 22 '11 at 13:59
  • 1
    If you feel more comfortable with WinForms you can code without MVVM which will be the conceptual the same as WinForms just the UI will be created by Xaml parser. Additionally I think it's a good idea to have also Expression Blend installed as it is much easier to accomplish many task e.g. editing styles. – bartosz.lipinski Jan 22 '11 at 14:34
  • Why was this question flagged "off topic"? It's definitely not OT... – Thomas Levesque Jan 22 '11 at 23:42
  • Interesting thing I've noted from the two answers so far - both of them use `Line` and not `Polyline`. I had tried to use polyline and bind `Polyline.Points` to the viewmodel. I came to a dead end with this, and it seems I needn't have sweated over it. – Tom W Jan 23 '11 at 12:50
  • 1
    Kudos to Rick Sladkey and vorrtex for the two answers they posted to this question. It's people like them that make stackoverflow an incredibly valuable resource. – Robert Rossney Jan 23 '11 at 19:10
  • @Robert, Word! Seeing this really drove it home for me. – dFlat Jul 21 '11 at 04:26

2 Answers2

26

I sympathize with you. Really understanding WPF takes a long time and it can be very frustrating to accomplish the simplest of things. But diving into a problem that is not easy for experts is only asking for trouble. You need to tackle simpler tasks and read a lot of code until things start to make sense. Donald Knuth says you don't really know the material until you do the exercises.

I solved your problem and I admit there are a lot of advance concepts in doing this cleanly and tacking on MVVM makes it that much harder. For what it's worth, here is a zero code-behind solution to your problem that is in the spirit of MVVM.

Here is the XAML:

<Grid>
    <Grid.Resources>
        <local:PolylineCollection x:Key="sampleData">
            <local:Polyline>
                <local:Coordinate X="50" Y="50"/>
                <local:Coordinate X="100" Y="100"/>
                <local:Coordinate X="50" Y="150"/>
            </local:Polyline>
        </local:PolylineCollection>
    </Grid.Resources>
    <Grid DataContext="{StaticResource sampleData}">
        <ItemsControl ItemsSource="{Binding Segments}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Line X1="{Binding Start.X}" Y1="{Binding Start.Y}" X2="{Binding End.X}" Y2="{Binding End.Y}" Stroke="Black" StrokeThickness="2"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <ItemsControl ItemsSource="{Binding ControlPoints}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Left" Value="{Binding X}"/>
                    <Setter Property="Canvas.Top" Value="{Binding Y}"/>
                </Style>
            </ItemsControl.ItemContainerStyle>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Ellipse Margin="-10,-10,0,0" Width="20" Height="20" Stroke="DarkBlue" Fill="Transparent">
                        <i:Interaction.Behaviors>
                            <local:ControlPointBehavior/>
                        </i:Interaction.Behaviors>
                    </Ellipse>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Grid>

and here are the supporting classes:

public class Coordinate : INotifyPropertyChanged
{
    private double x;
    private double y;

    public double X
    {
        get { return x; }
        set { x = value; OnPropertyChanged("X", "Point"); }
    }
    public double Y
    {
        get { return y; }
        set { y = value; OnPropertyChanged("Y", "Point"); }
    }
    public Point Point
    {
        get { return new Point(x, y); }
        set { x = value.X; y = value.Y; OnPropertyChanged("X", "Y", "Point"); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(params string[] propertyNames)
    {
        foreach (var propertyName in propertyNames)
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class Polyline : List<Coordinate>
{
}

public class Segment
{
    public Coordinate Start { get; set; }
    public Coordinate End { get; set; }
}

public class PolylineCollection : List<Polyline>
{
    public IEnumerable<Segment> Segments
    {
        get
        {
            foreach (var polyline in this)
            {
                var last = polyline.FirstOrDefault();
                foreach (var coordinate in polyline.Skip(1))
                {
                    yield return new Segment { Start = last, End = coordinate };
                    last = coordinate;
                }
            }
        }
    }

    public IEnumerable<Coordinate> ControlPoints
    {
        get
        {
            foreach (var polyline in this)
            {
                foreach (var coordinate in polyline)
                    yield return coordinate;
            }
        }
    }
}

public class ControlPointBehavior : Behavior<FrameworkElement>
{
    private bool mouseDown;
    private Vector delta;

    protected override void OnAttached()
    {
        var canvas = AssociatedObject.Parent as Canvas;
        AssociatedObject.MouseLeftButtonDown += (s, e) =>
        {
            mouseDown = true;
            var mousePosition = e.GetPosition(canvas);
            var elementPosition = (AssociatedObject.DataContext as Coordinate).Point;
            delta = elementPosition - mousePosition;
            AssociatedObject.CaptureMouse();
        };
        AssociatedObject.MouseMove += (s, e) =>
        {
            if (!mouseDown) return;
            var mousePosition = e.GetPosition(canvas);
            var elementPosition = mousePosition + delta;
            (AssociatedObject.DataContext as Coordinate).Point = elementPosition;
        };
        AssociatedObject.MouseLeftButtonUp += (s, e) =>
        {
            mouseDown = false;
            AssociatedObject.ReleaseMouseCapture();
        };
    }
}

This solution uses behaviors, which are ideal for implementing interactivity with MVVM.

If you are not familiar with behaviors, Install the Expression Blend 4 SDK and add this namespaces:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

and add System.Windows.Interactivity to your project.

Rick Sladkey
  • 33,988
  • 6
  • 71
  • 95
  • 2
    Thank you, this is a fantastic and very detailed example. Your taking the time to post it is very much appreciated. I've said the same to **vorrtex** because they're both very helpful. – Tom W Jan 23 '11 at 10:48
  • 1
    @Rick I wish I could upvote twice!! Thank you for taking the time to explain this concept and provide a detailed answer. Noooooow I get it. – dFlat Jul 21 '11 at 03:41
  • Wow. While I agree that this is not a question suited for StackOverflow since it is off topic>too broad I really have to admid that your answer pulled it around. Great stuff, man. Thank you. Works out of the box and is helpful. – anhoppe Mar 08 '16 at 09:16
13

I'll show how to build a WPF application with MVVM pattern for 2D-Poliline with draggable vertexes.

PointViewModel.cs

public class PointViewModel: ViewModelBase
{
    public PointViewModel(double x, double y)
    {
        this.Point = new Point(x, y);
    }

    private Point point;

    public Point Point
    {
        get { return point; }
        set
        {
            point = value;
            OnPropertyChanged("Point");
        }
    }
}

The class ViewModelBase contains only an implementation of interface INotifyPropertyChanged. This is necessary for reflecting changes of the clr-property on the visual representation.

LineViewModel.cs

public class LineViewModel
{
    public LineViewModel(PointViewModel start, PointViewModel end)
    {
        this.StartPoint = start;
        this.EndPoint = end;
    }

    public PointViewModel StartPoint { get; set; }
    public PointViewModel EndPoint { get; set; }
}

It has references to Points, so the changes will be received automatically.

MainViewModel.cs

public class MainViewModel
{
    public MainViewModel()
    {
        this.Points = new List<PointViewModel>
        {
            new PointViewModel(30, 30),
            new PointViewModel(60, 100),
            new PointViewModel(50, 120)
        };
        this.Lines = this.Points.Zip(this.Points.Skip(1).Concat(this.Points.Take(1)),
            (p1, p2) => new LineViewModel(p1, p2)).ToList();
    }

    public List<PointViewModel> Points { get; set; }
    public List<LineViewModel> Lines { get; set; }
}

It contains a sample data of points and lines

MainVindow.xaml

<Window.Resources>
    <ItemsPanelTemplate x:Key="CanvasPanelTemplate">
        <Canvas/>
    </ItemsPanelTemplate>
    <Style x:Key="PointListBoxItem">
        <Setter Property="Canvas.Left" Value="{Binding Point.X}"/>
        <Setter Property="Canvas.Top" Value="{Binding Point.Y}"/>
    </Style>
    <DataTemplate x:Key="LineTemplate">
        <Line X1="{Binding StartPoint.Point.X}" X2="{Binding EndPoint.Point.X}" Y1="{Binding StartPoint.Point.Y}" Y2="{Binding EndPoint.Point.Y}" Stroke="Blue"/>
    </DataTemplate>
    <DataTemplate x:Key="PointTemplate">
        <view:PointView />
    </DataTemplate>
</Window.Resources>
<Grid>
    <ItemsControl ItemsSource="{Binding Lines}" ItemsPanel="{StaticResource CanvasPanelTemplate}" ItemTemplate="{StaticResource LineTemplate}"/>
    <ItemsControl ItemsSource="{Binding Points}" ItemContainerStyle="{StaticResource PointListBoxItem}" ItemsPanel="{StaticResource CanvasPanelTemplate}"
                  ItemTemplate="{StaticResource PointTemplate}"/>
</Grid>

Here is a lot of tricks. First of all, these ItemsControls are based not on vertical StackPanel, but on Canvas. The ItemsControl of points applies a special container template with a goal to place items on necessary coordinates. But the ItemsControl of lines don't require such templates, and it is strange at some point. Two last DataTemplates are obvious.

PointView.xaml

<Ellipse Width="12" Height="12" Stroke="Red" Margin="-6,-6,0,0" Fill="Transparent"/>

Left and Top margins are equal to a half of the Width and the Height. We have a transparent Fill because this property doesn't have a default value and the events of mouse don't work.

That's almost all. Only drag-n-drop functionality remains.

PointView.xaml.cs

public partial class PointView : UserControl
{
    public PointView()
    {
        InitializeComponent();

        this.MouseLeftButtonDown += DragSource_MouseLeftButtonDown;
        this.MouseMove += DragSource_MouseMove;
    }

    private bool isDraggingStarted;

    private void DragSource_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        this.isDraggingStarted = true;
    }

    private void DragSource_MouseMove(object sender, MouseEventArgs e)
    {
        if (isDraggingStarted == true)
        {
            var vm = this.DataContext as PointViewModel;
            var oldPoint = vm.Point;

            DataObject data = new DataObject("Point", this.DataContext);
            DragDropEffects effects = DragDrop.DoDragDrop(this, data, DragDropEffects.Move);

            if (effects == DragDropEffects.None) //Drag cancelled
                vm.Point = oldPoint;

            this.isDraggingStarted = false;
        }
    }

MainVindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MainViewModel();

        this.AllowDrop = true;
        this.DragOver += DropTarget_DragOver;

    }

    private void DropTarget_DragOver(object sender, DragEventArgs e)
    {
        var vm = e.Data.GetData("Point") as PointViewModel;
        if (vm != null)
            vm.Point = e.GetPosition(this);
    }
}

So your sample is done using 2 xaml files and 3 viewmodels.

vortexwolf
  • 13,967
  • 2
  • 54
  • 72
  • Thank you, this is a fantastic and very detailed example. Your taking the time to post it is very much appreciated. I've said the same to **Rick Sladkey** because they're both very helpful. – Tom W Jan 23 '11 at 10:49