How to properly bind an ObservableCollection inside an ObservableCollection to an ItemsControl (WPF)

43 Views Asked by At

Before I start with the explanation of the problem, I am new to WPF and I still did not grasp every concept behind the MVVM pattern.

I have an ObservableCollection<Vertex>, where Vertex is a class representing a vertex in a graph. It contains properties representing its position: X,Y and Point Position. It also contains an ObservableCollection<Edge> where Edge is a simple model containing 2 Vertices (From and To) and its weight as an int.

My intention is to display both the collection of vertices (as ellipses) and then also display the edges as lines. The view I am trying to create should allow for addition of these vertices and edges, which I am doing through commands since I am trying to follow the MVVM architecture.

The vertices can be added without any problems, however the problems start when I am trying to add an edge from one vertex to another. The first selected vertex for the edge gets moved to a completely different spot on the view (+ on both x and y axis) and the line representing the edge has the correct slope, but also moved somewhere else.

Here is a brief demonstration of what I mean: Situation before the addition of an edge Situation after the addition of an edge

This is what I currently have in the xaml file for the view with the canvas (only the important part with the ItemsControl):

<ItemsControl Grid.Row="1" Grid.Column="1" ItemsSource="{Binding Graph.Vertices}" Background="AliceBlue">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseLeftButtonDown">
                    <commands:MouseBinding Command="{Binding CanvasClickCommand}"></commands:MouseBinding>
                </i:EventTrigger>
            </i:Interaction.Triggers>
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas IsItemsHost="True">
                    </Canvas>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemContainerStyle>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Left" Value="{Binding X}"></Setter>
                    <Setter Property="Canvas.Top" Value="{Binding Y}"></Setter>
                </Style>
            </ItemsControl.ItemContainerStyle>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Ellipse Width="10" Height="10">
                            <Ellipse.Style>
                                <Style TargetType="Ellipse">
                                    <Setter Property="Fill" Value="Red" />
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding IsSelected}" Value="True">
                                            <Setter Property="Fill" Value="Green" />
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Ellipse.Style>
                        </Ellipse>
                        <ItemsControl ItemsSource="{Binding Edges}">
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                    <Path Stroke="Black" >
                                        <Path.Data>
                                            <LineGeometry StartPoint="{Binding FromVertex.Position}" EndPoint="{Binding ToVertex.Position}" />
                                        </Path.Data>
                                    </Path>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

And this is what I have in the ViewModel file for this:

public class SceneEditorViewModel : BaseViewModel
    {
        private enum Mode
        {
            VertexAdditionMode,
            EdgeAdditionMode,
            ViewMode,
            VertexDeletionMode
        }

        private readonly SceneEntry _sceneEntry;

        private Mode _mode = Mode.ViewMode;
        private Vertex? _firstClickedVertex;
        public IGraph Graph => _sceneEntry.Graph;

        /// <summary>
        /// Commands for Buttons
        /// </summary>
        public ICommand LoadFileCommand { get; }
        public ICommand SetNameCommand { get; }
        public ICommand BackCommand { get; }
        public ICommand AddVertexCommand { get; }
        public ICommand AddEdgeCommand { get; }
        public ICommand ClearCommand { get; }
        public ICommand DeleteVertexCommand { get; }
        /// <summary>
        /// Command called after user clicks on the canvas area
        /// </summary>
        public ICommand CanvasClickCommand { get; }
        public string NameTextBox
        {
            get => _sceneEntry.Name;
            set
            {
                _sceneEntry.Name = value;
                OnPropertyChanged();
            }
        }

        public SceneEditorViewModel(NavigationManager navigationManager, SceneEntry? entry = null)
        {
            LoadFileCommand = new RelayCommand(LoadFile, o => true);
            SetNameCommand = new RelayCommand(SetName, o => true);
            BackCommand = new NavigationCommand<SceneMenuViewModel>(new NavigationService<SceneMenuViewModel>(navigationManager, 
                () => new SceneMenuViewModel(navigationManager)));
            AddVertexCommand =
                new RelayCommand(
                    o => _mode = _mode == Mode.VertexAdditionMode ? Mode.ViewMode : Mode.VertexAdditionMode,
                    o => true);
            AddEdgeCommand =
                new RelayCommand(
                    o => _mode = _mode == Mode.EdgeAdditionMode ? Mode.ViewMode : Mode.EdgeAdditionMode,
                    o => true);
            CanvasClickCommand = new EventRelayCommand<MouseButtonEventArgs>(OnMouseDown, o => true);
            DeleteVertexCommand = new RelayCommand(DeleteVertex, o => true);
            _sceneEntry = entry ?? new SceneEntry("", SceneMenuViewModel.SceneIdCounter++, new Graph());
            ClearCommand = new RelayCommand(o => _sceneEntry.Graph.Vertices.Clear(), o => true);
            if (entry is null)
            {
                App.Scenes.Add(new SceneEntryViewModel(navigationManager, _sceneEntry));
            }
        }

        private void LoadFile(object obj)
        {
            var openFileDialog = new OpenFileDialog
            {
                Filter = "Graph files (*.graph)|*.graph"
            };
            if (openFileDialog.ShowDialog() == true)
            {
                var file = openFileDialog.FileName;
                var serializer = new GraphSerializer();
                var graph = serializer.Deserialize(file);
                _sceneEntry.Graph = graph;
            }


            //var serializer = new Serializer();
            //var graph = serializer.Deserialize(OpenFileDialog.FileName);
            //_graphViewModel.Graph = graph;
        }

        private void SetName(object obj)
        {
            if (NameTextBox != "")
            {
                _sceneEntry.Name = NameTextBox;
            }
        }

        private void DeleteVertex(object obj)
        {
            _mode = _mode == Mode.VertexDeletionMode ? Mode.ViewMode : Mode.VertexDeletionMode;
        }
        private void OnMouseDown(MouseButtonEventArgs e)
        {
            var sender = e.Source as IInputElement;
            var position = e.GetPosition(sender);
            switch (_mode)
            {
                case Mode.VertexAdditionMode:
                    var vertex = new Vertex("", position.X, position.Y, new ObservableCollection<Edge>());
                    _sceneEntry.Graph.AddVertex(vertex);
                    break;
                case Mode.EdgeAdditionMode:
                    var first = _sceneEntry.Graph.Vertices.FirstOrDefault(v =>
                        v.X - 10 < position.X && position.X < v.X + 10 && v.Y - 10 < position.Y &&
                        position.Y < v.Y + 10);
                    if (first is null) break;
                    if (_firstClickedVertex is null )
                    {
                        first.IsSelected = true;
                        _firstClickedVertex = first;
                    }
                    else
                    {
                        if (first != _firstClickedVertex)
                        {
                            var edge = new Edge()
                            {
                                FromVertex = new Vertex(_firstClickedVertex),
                                ToVertex = new Vertex(first),
                                IsDirected = false,
                                Weight = 1
                            };
                            _sceneEntry.Graph.Vertices.First(v => v == _firstClickedVertex).Edges.Add(edge);
                        }
                        _firstClickedVertex.IsSelected = false;
                        _firstClickedVertex = null;
                    }
                    break;
                case Mode.VertexDeletionMode:
                    var selected = _sceneEntry.Graph.Vertices.FirstOrDefault(v =>
                        v.X - 10 < position.X && position.X < v.X + 10 && v.Y - 10 < position.Y &&
                        position.Y < v.Y + 10);
                    if (selected is null) break;
                    _sceneEntry.Graph.Vertices.Remove(selected);
                    break;
                default:
                    break;
            }
        }

    }

The code is really messy (mainly due to trying to figure out what is the exact problem), but the intention should be somewhat visible.

The Graph class is basically just a class encapsulating the ObservableCollection and all of the models implement INotifyPropertyChaned interface.

Am I on the right path with this approach inside of the xaml file or is there a much better way to do what I want to do?

0

There are 0 best solutions below