C#, WPF Bindowanie, dwa okna, dziwna sprawa

0

Person.cs

using System.ComponentModel;

namespace MyWPFProject
{
    public class Person : INotifyPropertyChanged
    {
        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                name = value;
                OnPropertyChanged(nameof(Name));
            }
        }
        public string Surname
        {
            get
            {
                return surname;
            }
            set
            {
                surname = value;
                OnPropertyChanged(nameof(Surname));
            }
        }
        public int Age
        {
            get
            {
                return age;
            }
            set
            {
                age = value;
                OnPropertyChanged(nameof(Age));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
                handler(this, new PropertyChangedEventArgs(propertyName));
        }

        private string name;
        private string surname;
        private int age;
    }
}
 

RelayCommand.cs

 using System;
using System.Windows.Input;

namespace MyWPFProject
{
    public class RelayCommand : ICommand
    {
        private readonly Func<bool> _canExecute;
        private readonly Action _execute;

        public RelayCommand(Action execute)
            : this(execute, null)
        {
        }

        public RelayCommand(Action execute, Func<bool> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");
            _execute = execute;
            _canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (_canExecute != null)
                    CommandManager.RequerySuggested += value;
            }
            remove
            {
                if (_canExecute != null)
                    CommandManager.RequerySuggested -= value;
            }
        }

        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute();
        }

        public void Execute(object parameter)
        {
            _execute();
        }
    }
}

MainWindow.xaml

<Window x:Class="MyWPFProject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MyWPFProject"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Grid>
        <StackPanel>
            <ListView Name="listView" ItemsSource="{Binding Path=ListOfPeople, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Path=Name}"/>
                        <GridViewColumn Header="Surname" DisplayMemberBinding="{Binding Path=Surname}"/>
                        <GridViewColumn Header="Age" DisplayMemberBinding="{Binding Path=Age}"/>
                    </GridView>
                </ListView.View>
            </ListView>
            <Button FontSize="20" MinHeight="30" Margin="180,20,180,0" Content="Zmień" Command="{Binding ChangeData}"/>
            <Button FontSize="20" MinHeight="30" Margin="180,20,180,0" Content="Dodaj osobę" Command="{Binding AddPersonWindowOpen}"/>
        </StackPanel>
    </Grid>
</Window>

AddPersonWindow.xaml

<Window x:Class="MyWPFProject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MyWPFProject"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:ViewModel/>
    </Window.DataContext>
    <Grid>
        <StackPanel>
            <ListView Name="listView" ItemsSource="{Binding Path=ListOfPeople, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
                <ListView.View>
                    <GridView>
                        <GridViewColumn Header="Name" DisplayMemberBinding="{Binding Path=Name}"/>
                        <GridViewColumn Header="Surname" DisplayMemberBinding="{Binding Path=Surname}"/>
                        <GridViewColumn Header="Age" DisplayMemberBinding="{Binding Path=Age}"/>
                    </GridView>
                </ListView.View>
            </ListView>
            <Button FontSize="20" MinHeight="30" Margin="180,20,180,0" Content="Zmień" Command="{Binding ChangeData}"/>
            <Button FontSize="20" MinHeight="30" Margin="180,20,180,0" Content="Dodaj osobę" Command="{Binding AddPersonWindowOpen}"/>
        </StackPanel>
    </Grid>
</Window>

W MainWindow.xaml.cs i AddWindow.xaml.cs nic nie mam poza InitializeComponents()

I najważniejsze czyli ViewModel

ViewModel.cs

 using System.ComponentModel;
using System.Windows.Input;
using System.Collections.ObjectModel;
using System.Windows;

namespace MyWPFProject
{
    public class ViewModel : INotifyPropertyChanged
    {
        public ViewModel()
        {
            ListOfPeople = new ObservableCollection<Person>();
            ListOfPeople.Add(new Person() { Name = "Michael", Surname = "Jordan", Age = 22 });
            ListOfPeople.Add(new Person() { Name = "Jan", Surname = "Kowalski", Age = 21 });
            ListOfPeople.Add(new Person() { Name = "Anna", Surname = "Nowak", Age = 20 });
        }

        private void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if(handler!=null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private void ChangeDataExecute()
        {
            ListOfPeople[1].Name = "Marek";
            ListOfPeople[1].Surname = "Adamiak";
            ListOfPeople[1].Age = 24;

             //Tutaj natomiast jakbym to odkomentowal to po wcisnieciu buttona zmien wszystko dziala, tzn View tez sie aktualizuje
            //ListOfPeople.Add(new Person() { Name = "Jan" });
        }

        private void AddPersonWindowOpenExecute()
        {
            AddPersonWindow addPersonWindow = new AddPersonWindow();
            addPersonWindow.Show();
        }

        private void AddPersonToListExecute()
        {
            foreach(Window window in Application.Current.Windows)
            {
                if (window.GetType() == typeof(AddPersonWindow))
                {
                    //I tu lezy problem. Sprawdzalem debuggerem ze element jest prawidlowo dodawany do listy, ale nie View w MainWindow nie jest odswiezany
                    ListOfPeople.Add(new Person()
                    {
                        Name = (window as AddPersonWindow).nameTextBox.Text,
                        Surname=(window as AddPersonWindow).surnameTextBox.Text,
                        Age = int.Parse((window as AddPersonWindow).ageTextbox.Text)
                    });

                    (window as AddPersonWindow).Close();
                }
            }
        }

        public ICommand ChangeData { get { return new RelayCommand(ChangeDataExecute); } }
        public ICommand AddPersonWindowOpen { get { return new RelayCommand(AddPersonWindowOpenExecute); } }
        public ICommand AddPersonToList { get { return new RelayCommand(AddPersonToListExecute); } }

        public ObservableCollection<Person> ListOfPeople
        {
            get
            {
                return listOfPeople;
            }
            set
            {
                listOfPeople = value;
                OnPropertyChanged("ListOfPeople");
            }
        }

        private ObservableCollection<Person> listOfPeople;

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Mam tu prostą aplikację w WPF, która obsługuje 2 okna.

228f75cd92.png

Jak wciskam button DodajOsobe to otwiera sie nowe okno

c68f9c8d39.png

Mogę teraz wpisać nową osobę. Jak to zrobię to chcę, żeby ona była dodana do listy i tak się dzieje, bo sprawdzałem debuggerem że w ViewModelu wlasciwość ListOfPeople się aktualizuje (ma teraz 4 elementy), ale nie aktualizuje mi się to w View w głównym oknie. Natomiast jak w funkcji ChangeDataExecute dodam tam element do listy to wszystko działa, tzn. View się aktualizuje. Dlaczego tak się dzieje?

1

Na końcu metody AddPersonToListExecute() dodaj OnPropertyChanged("ListOfPeople").
Notyfikacja w seterze ListOfPeople nie działa, gdy zmienia się liczba elementów w kolekcji.

0

@lysy Twoja metoda nie działa niestety

1

Może zerknę na to trochę później jak znajdę dziś czas ale łamiesz w swoim kodzie zasady MVVM.

  • Do Viewmodelu nie przekazuje się kontrolek gdyż Viewmodel nie wie nic o widoku, a jedynie dostarcza mu dane. Mowa o metodzie AddPersonToListExecute gdzie przekazujesz Window. To podstawowy błąd.
1

Jeżeli chcesz w drugim oknie edytować wartości listy to masz dwa wyjścia:

  • Bliżej mvvm - zastosowanie wzorca mediatora;
  • Bliżej mvvm ale trochę bardziej niechlujnie - zrobienie singletona, do którego wsadzisz listę i pobierzesz w oknie do modyfikacji;
  • Trochę dalej od mvvm ale bardziej klasycznie - przekazanie listy przez konstruktor okna do viewmodelu.

Jako, że klasa jest typem referencyjnym to wprowadzenie zmian w oknie zaowocuje wprowadzeniem ich od razu na elemencie listy, a OnPropertyChanged zadba o aktualizację widoku.

PS: Tylko jak wspomniałem... nie mam trochę teraz na to czasu, bo w pracy jestem.

0

Siema. Już naprawiłem błąd. Problem był prosty. Zarówno w oknie głównym (MainWindow) jak i w oknie do dodawania osoby tworzyłem osobny DataContext, a przecież powinny korzystać ze wspólnego kontekstu do bindowania :)

0

AddPersonWindow.xaml.cs

    /// <summary>
    /// Interaction logic for AddPersonWindow.xaml
    /// </summary>
    public partial class AddPersonWindow : Window
    {
        public AddPersonWindow(VM vm)
        {
            InitializeComponent();
            DataContext = vm;
        }
    }

 

No i w xamlu tego okna trzeba wywalić DataContext.

VM.cs (zmieniłem nazwę pliku ViewModel na VM)

  private void AddPersonWindowOpenExecute()
        {
            AddPersonWindow addPersonWindow = new AddPersonWindow(this);
            addPersonWindow.Show();
        } 

Reszta bez zmian. Tak, wiem że otwierając nowe okno w ViewModelu oraz odnosząc się do kontrolki w ViewModelu łamię MVVM, ale spróbuje ogarnąć jak działa ten mediator. Jak masz jakieś dobre linki to obczaję.

1

To właśnie też jest nieprawidłowe podejście wg mvvm. Popatrz... to co robisz to integrujesz widok z viewmodelem wywołując wewnątrz niego nowe okna. Tak jak mówiłem jest to błąd gdyż wg mvvm viemodel nie wie nic o widoku, którego dowolnie można zmieniać i tak naprawdę sam viewmodel powinien działać nie wykorzystując GUI (choć nie zawsze jest to reguła). Jak temu zaradzić? Można posłużyć się eventem, który zostanie wywołany po wciśnięciu przycisku, który odpala komendę. Do wywoływania nowych okien służy code-behind, gdyż to on obsługuje widok i nie jest prawdą to co twierdzą puryści, że ma zawszy być pusty.

Ja bym to zrobił w ten sposób:
http://www.filedropper.com/4plistofpeople

Jest to przykład z przekazywaniem parametrów przez konstruktor czyli najprostszy ale... nie w pełni zgodny z wzorcem :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1