[WPF] Obiekt danej klasy zbindowany do DataContext GroupBoxa nie zawsze zawiera bieżące wartości

0

Witam,

mam dość dziwny problem z bindowaniem w WPFie. Pokażę i opiszę na przykładzie:

//w klasie kontrolki
class PersonForm : UserControl
{
  public Person person = new Person();
  public PersonForm()
  {
    person.name = "XXX";
    person.last_name = "XXXX";
    myGroupBox.DataContext = person;
  }

  private Person GetPersonFromForm() //użycie tej metody wewnątrz klasy UserControl zwróci aktualne dane na podstawie zmian w formularzu
  {
    return person; 
  }

  public Person GetPersonFromForm2() //użycie tej metody z zewnątrz zwróci stare dane, które zostały wcześniej zbindowane do DataContext
  {
    return person; 
  }
}

class Person()
{
  public string name { get; set;}
  public string last_name { get; set;}
}

//w innej klasie

Person person = new Person();

private void GetPersonFromForm()
{
  person = MainWindow.personForm.person;
  person = MainWindow.personForm.GetPersonFromForm2();
  //obie linie zwracają do "person" stare dane
}

Mam GroupBox, do którego zbindowałem obiekt klasy Person. W GroupBoxie jest kilka TextBoxów odpowiadających odpowiednim polom obiektu. Chcę pobrać aktualne wartości obiektu zbindowanego do GroupBoxa tj. danych zmienionych po zbindowaniu obiektu. Wewnątrz klasy tej kontrolki nie ma najmniejszego problemu i uzyskuje aktualne wartości po prostu biorąc obiekt "person". W przypadku sięgania do tego publicznego obiektu z zewnątrz (tj. innej klasy) mam kłopot, bo otrzymuje dokładnie te wartości, które na początku dodałem do obiektu, zamiast aktualnych z TextBoxów.

Dlaczego tak jest? To jakaś specyfika działania DataContext? Zabezpieczenie przed wyciąganiem danych?

0

Musisz zaimplementować inotifypropertychanged. Zeby binding działał lub korzystać depedency properties

    {  
        public event PropertyChangedEventHandler PropertyChanged;  
        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")  
        {  
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }  

Każde property które chcesz regowało na zmiany w bindigu, powinno wołać, na końcu set'era NotifyPropertyChanged(), lub NotifyPropertyChanged(nazwa), jeśli zmienia coś więcej niż samo siebie. Dzięki temu implementacja wpf wie do czego sie podpiąc i propaguję zmiany.

polecam napisać sobie własny codeSnipset, który sam bedzie generował Ci potrzebny borderplate bo to naprawdę robi się smutne, już po 10 razach. DependecyPropery uzywa sie żeby kontrolki posiadały bindowalne pola, tzn można w nie coś w bidowac. To jest jeszcze bardziej smutny kod, naszczeście wystarczy napisac propdp i dodać 2 razy tab szybko, i wystarczy uzupełnić.

0
_flamingAccount napisał(a):

Musisz zaimplementować inotifypropertychanged. Zeby binding działał lub korzystać depedency properties

    {  
        public event PropertyChangedEventHandler PropertyChanged;  
        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")  
        {  
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }  

Każde property które chcesz regowało na zmiany w bindigu, powinno wołać, na końcu set'era NotifyPropertyChanged(), lub NotifyPropertyChanged(nazwa), jeśli zmienia coś więcej niż samo siebie. Dzięki temu implementacja wpf wie do czego sie podpiąc i propaguję zmiany.

polecam napisać sobie własny codeSnipset, który sam bedzie generował Ci potrzebny borderplate bo to naprawdę robi się smutne, już po 10 razach. DependecyPropery uzywa sie żeby kontrolki posiadały bindowalne pola, tzn można w nie coś w bidowac. To jest jeszcze bardziej smutny kod, naszczeście wystarczy napisac propdp i dodać 2 razy tab szybko, i wystarczy uzupełnić.

Generalnie fajnie, ale pojawia się jeden problem: to tylko przykład. Tak naprawdę w projekcie mam model wygenerowany przez Entity i obiekt tego modelu binduje do GroupBoxa (a pola do TextBoxów w tym GroupBoxie). A jako, że mam DbFirst, musiałbym przepisywać ten event za każdym razem, kiedy wygeneruję nowy model z bazy, która to niestety często się zmienia. Jeszcze gorzej, że tych pól jest sporo.

Jednak dla mnie nie jest to bezpośrednie rozwiązanie problemu, ponieważ jak pisałem wyżej, wewnątrz klasy kontrolki wywołując obiekt modelu, otrzymuje w obiekcie aktualne wartości pól z formularza. Poza klasą kontrolki dostaje stary obiekt, nieważne czy chcę go bezpośrednio, czy też zwrócić publiczną metodą.

2

Tak naprawdę w projekcie mam model wygenerowany przez Entity i obiekt tego modelu binduje do (...) mam DbFirst, musiałbym przepisywać ten event za każdym razem, kiedy wygeneruję nowy model z bazy

Dlatego powinienes rodzielić model od viewModelu, problem zniknie, i recznie będziesz musiał edytować tylko te zmiany które tego wymagają. Może w Twoim przypadku da sie na samych modelach ale przy czymś bardziej złożonym będzie to problematyczne.

Co do głownego problemu musisz mieć bug'a gdzieś po swojej stronie, bo kod z przykładu zwraca te samą instacje, co musi działać. Jesteś pewien ze w żadnej tych metod nie kopiujesz danych lub nie tworzysz nowego person? oraz ze MainWindow to nie nowa instancja? oraz że na pewno przekazujesz właściwie dane miedzy MainWindow, a userControl? oraz że nowo utworzony person w innej klasie nie zostaje magicznie wciśniety do widoku(który sie nie odświerza) i tworzenie nowej instacji/wołanie metody resetuje dane?

0
_flamingAccount napisał(a):

Tak naprawdę w projekcie mam model wygenerowany przez Entity i obiekt tego modelu binduje do (...) mam DbFirst, musiałbym przepisywać ten event za każdym razem, kiedy wygeneruję nowy model z bazy

Dlatego powinienes rodzielić model od viewModelu, problem zniknie, i recznie będziesz musiał edytować tylko te zmiany które tego wymagają. Może w Twoim przypadku da sie na samych modelach ale przy czymś bardziej złożonym będzie to problematyczne.

Co do głownego problemu musisz mieć bug'a gdzieś po swojej stronie, bo kod z przykładu zwraca te samą instacje, co musi działać. Jesteś pewien ze w żadnej tych metod nie kopiujesz danych lub nie tworzysz nowego person? oraz ze MainWindow to nie nowa instancja? oraz że na pewno przekazujesz właściwie dane miedzy MainWindow, a userControl? oraz że nowo utworzony person w innej klasie nie zostaje magicznie wciśniety do widoku(który sie nie odświerza) i tworzenie nowej instacji/wołanie metody resetuje dane?

Co do pytań:

  1. Nie. Zwracam dokładnie ten publiczny obiekt z dokładnie tej instancji klasy UserControl, której formularz jest na ekranie.
  2. Sprawdziłem przekazując instancję MainWindow (na 100% tą, ponieważ wywołuje ją z aktywnej instancji MainWindow) do danej klasy, nic się nie zmieniło.
  3. Co to znaczy "właściwie"? MainWindow zawiera instancję UserControl, w której jest publiczny obiekt. Ten obiekt właśnie pobieram.
  4. Obiekt istnieje tylko w jednej instancji, innych instancji nie tworzę, ponieważ ładuję UserControl na początku programu i mam tą instancję zapisane.

Dodam też, że w metodzie publicznej zwracającej obiekt dodałem linijkę wczytującą tekst z jednego TextBoxa i sprawdziłem linijka po linijce, co się dzieje. Z TextBoxa otrzymuje aktualną wartość, metoda wywołana z zewnątrz dosłownie daje mi nieaktualny obiekt. Metoda wewnątrz UserControl daje mi aktualny obiekt. Nic nie dzieje się po drodze, najczystsze wręcz odwołanie do publicznego obiektu, czy do metody, daje ten sam efekt - obiekt jest nieaktualny.
Nawet nie wiem, czy wprowadzanie ww. eventu da jakąkolwiek różnicę, ponieważ odwołując się z poza klasy obiekt reaguje, jakby nie zaszły żadne zmiany.

Edit: Jeszcze jedna uwaga. Wywołanie wewnątrz klasy kontrolki triggeruje aktualizację obiektu, wywołanie z zewnątrz nie triggeruje.

1

Pokażę to na prostym przykładzie:
Klasa po której dziedziczy się interfejs INotifyPropertyChanged, aby informować widok o zmianie właściwości

public abstract class NotifyPropertyChanged : INotifyPropertyChanged
{
        public event PropertyChangedEventHandler PropertyChanged;  
        private void NotifyPropertyChanged(string propertyName = "")  
        {  
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }  
}

I teraz tak: mamy klasę person, która odziedziczyła interfejs INotifyPropertyChanged w której są właściwości

public class Person : NotifyPropertyChanged : INotifyPropertyChanged
{
     public Person(string Age)
     {
          this.Age = Age;
     }
     private string age;
     public string Age
     {
          get { return age; }
          set { age = value; NotifyPropertyChanged("Age"); } // Wywołanie metody NotifyPropertyChanged powiadamia twój widok, że właściwość zmieniła się i zawartość musi zostać odświeżona
     }
}

Ale teraz popatrz na inny przykład:

public class AnyClass : NotifyPropertyChanged, INotifyPropertyChanged
{
     public Person person1 = new Person("Kowalski1");
     public Person person2 { get; private set; } = new Person("Kowalski2");
     private Person person3 = new Person("Kowalski3");
     public Person Person3
     {
          get { return person3; }
          set { person3= value; NotifyPropertyChanged("Person3"); }
     }
}

Do "person1" nie da się wcale bindować.
W "person2" będzie odświeżany widok tylko jeżeli bindujesz do "person2.Age", ale jeżeli bindujesz do "person2" i przypiszesz jej inną wartość nic Ci się nie odświeży.
natomiast "Person3" będzie odświeżane zarówno wtedy jak zmienisz obiekt jak i wartość w obiekcie.

mam nadzieje, że ten prosty przykład rozjaśni sprawę

0
Grzegorz Świdwa napisał(a):

Pokażę to na prostym przykładzie:
Klasa po której dziedziczy się interfejs INotifyPropertyChanged, aby informować widok o zmianie właściwości

public abstract class NotifyPropertyChanged : INotifyPropertyChanged
{
        public event PropertyChangedEventHandler PropertyChanged;  
        private void NotifyPropertyChanged(string propertyName = "")  
        {  
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }  
}

I teraz tak: mamy klasę person, która odziedziczyła interfejs INotifyPropertyChanged w której są właściwości

public class Person : NotifyPropertyChanged : INotifyPropertyChanged
{
     public Person(string Age)
     {
          this.Age = Age;
     }
     private string age;
     public string Age
     {
          get { return age; }
          set { age = value; NotifyPropertyChanged("Age"); } // Wywołanie metody NotifyPropertyChanged powiadamia twój widok, że właściwość zmieniła się i zawartość musi zostać odświeżona
     }
}

Ale teraz popatrz na inny przykład:

public class AnyClass : NotifyPropertyChanged, INotifyPropertyChanged
{
     public Person person1 = new Person("Kowalski1");
     public Person person2 { get; private set; } = new Person("Kowalski2");
     private Person person3 = new Person("Kowalski3");
     public Person Person3
     {
          get { return person3; }
          set { person3= value; NotifyPropertyChanged("Person3"); }
     }
}

Do "person1" nie da się wcale bindować.
W "person2" będzie odświeżany widok tylko jeżeli bindujesz do "person2.Age", ale jeżeli bindujesz do "person2" i przypiszesz jej inną wartość nic Ci się nie odświeży.
natomiast "Person3" będzie odświeżane zarówno wtedy jak zmienisz obiekt jak i wartość w obiekcie.

mam nadzieje, że ten prosty przykład rozjaśni sprawę

  1. Chcę tylko pobrać obiekt z aktualnymi wartościami, nie chcę obiektu modyfikować w żaden sposób.
  2. INotifyPropertyChanged to jest rozwiązanie na około i w żaden sposób nie wyjaśnia, jak odwołanie z zewnątrz klasy nie triggeruje zmian w obiekcie, kiedy wywołanie wewnątrz klasy już to robi. Wygląda to jak łatanie czegoś, co powinno działać naturalnie bez względu na to, skąd sięgam do obiektu. Wiem, jak działa INotifyPropertyChanged, naprawdę nie musicie mi pokazywać przykładów, ponieważ nie zmienia to absolutnie problemu. Rozwiązania na około mnie nie interesują.
1

No to jak tak to wytłumacz mi:

private Person GetPersonFromForm() //użycie tej metody wewnątrz klasy UserControl zwróci aktualne dane na podstawie zmian w formularzu
  {
    return person; 
  }

  public Person GetPersonFromForm2() //użycie tej metody z zewnątrz zwróci stare dane, które zostały wcześniej zbindowane do DataContext
  {
    return person; 
  }

Oraz problem:

private void GetPersonFromForm()
{
  person = MainWindow.personForm.person;
  person = MainWindow.personForm.GetPersonFromForm2();
  //obie linie zwracają do "person" stare dane
}

Czym się różnią te metody, aby zwrócić inne dane?

0
Grzegorz Świdwa napisał(a):

No to jak tak to wytłumacz mi:

private Person GetPersonFromForm() //użycie tej metody wewnątrz klasy UserControl zwróci aktualne dane na podstawie zmian w formularzu
  {
    return person; 
  }

  public Person GetPersonFromForm2() //użycie tej metody z zewnątrz zwróci stare dane, które zostały wcześniej zbindowane do DataContext
  {
    return person; 
  }

Oraz problem:

private void GetPersonFromForm()
{
  person = MainWindow.personForm.person;
  person = MainWindow.personForm.GetPersonFromForm2();
  //obie linie zwracają do "person" stare dane
}

Czym się różnią te metody, aby zwrócić inne dane?

Są tam komentarze, przydałoby się je przeczytać. Opis problemu również.
A metody różnią się niczym. Różni się jedynie miejsce, w którym miałyby być wywołane. Jak pisałem wyżej, metoda (czy sam obiekt) wywołany wewnątrz klasy otrzymuje z dokładnie tymi samymi danymi, które są w formularzu, czyli zaktualizowały się. Metoda czy obiekt wywołana z poza klasy nie aktualizuje danych przed ich pobraniem.

0

Jak zrobisz to po ludzku to nie będziesz miał problemów:
Zobacz ten przykład:

UserControl:

public partial class UC : UserControl
    {
        public UC()
        {
            InitializeComponent();
            NewPerson = new Person("Kowalski");
            OldPerson = new Person("Kowalski");
        }



        public string Surname
        {
            get { return (string)GetValue(SurnameProperty); }
            set { SetValue(SurnameProperty, value); }
        }
        public static readonly DependencyProperty SurnameProperty =
            DependencyProperty.Register("Surname", typeof(string), typeof(UC), new PropertyMetadata(string.Empty, (DependencyObject d, DependencyPropertyChangedEventArgs e) => {
                (d as UC).SurnameChanged((e.NewValue == null) ? string.Empty : e.NewValue.ToString());
            }));
        internal void SurnameChanged(string NewSurname)
        {
            Dispatcher?.Invoke(() =>
            {
                OldPerson = NewPerson;
                NewPerson = new Person(NewSurname);
            });
        }
        

        public Person NewPerson
        {
            get { return (Person)GetValue(NewPersonProperty); }
            set { SetValue(NewPersonProperty, value); }
        }
        public static readonly DependencyProperty NewPersonProperty =
            DependencyProperty.Register("NewPerson", typeof(Person), typeof(UC), new PropertyMetadata(null));

        public Person OldPerson
        {
            get { return (Person)GetValue(OldPersonProperty); }
            set { SetValue(OldPersonProperty, value); }
        }
        public static readonly DependencyProperty OldPersonProperty =
            DependencyProperty.Register("OldPerson", typeof(Person), typeof(UC), new PropertyMetadata(null));



        public class Person : NotifyPropertyChanged, INotifyPropertyChanged
        {
            public Person(string Surname)
            {
                this.Surname = Surname;
            }
            private string surname;
            public int Surname { get { return surname; } set { surname = value; PropertyChanged("Surname"); } }
        }
    }
UserControl x:Class="WpfApp.UC"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp"
             x:Name="hWindow"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <TextBox Text="{Binding ElementName=hWindow, Path=Surname, Mode=TwoWay, UpdateSourceTrigger=LostFocus}"/>
    </Grid>
</UserControl>

MainWindow:

<Window x:Class="WpfApp.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:WpfApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <local:UC x:Name="hUC"/>
    </Grid>
</Window>
namespace WpfApp
{
    /// <summary>
    /// Logika interakcji dla klasy MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            Person Actual = hUC.NewPerson;
            Person Old = hUC.OldPerson;
        }
    }
}

To i tak nie jest do końca po bożemu ale lepiej niż ustalanie DataContext w kodzie

EDIT:
Założyciel tematu napisał, że nie wyjaśniłem nic.
Dlatego na starcie poczytaj o:

  1. Oddzieleniu warstwy widoku od warstwy logicznej - To na całkowity start.
  2. Pakuj ile się da do XAMLA. Jeżeli dasz radę bindować nie przypisuj zmiennej w warstwie logicznej do widoku. jeżeli zmienne mają różny typ używaj konwerterów.
  3. Poczytaj o wcześniej wspomnianym INotifyPropertyChanged oraz DependencyProperty
  4. A to najważniejsze:

jeżeli chcesz dobrej odpowiedzi w swoim wątku nie wstawiaj pytania typu:

Dlaczego metoda Start() zwraca null?

public object Start
{
      //Tutaj jest kod ale nie chce mi sie wklejać całego projektu
}

Musisz podać więcej informacji. Akurat jeżeli chodzi o Twój wątek bardzo ważne jest co się dzieje w warstwie widoku, więc wstaw również kod XAML.. Jeżeli kod jest bardzo duży wstaw go na pastebin.com

1

Ja skończyłem pomagać w tym wątku. Człowiek specjalnie vs odpala, stara się pomóc mając zero informacji a w rezultacie autor wypisuje, żebym sobie darował. Wypisuje to po kilkudziesięciu minutach. Na 100% ma w dupie to co napisałem i nawet nie sprawdził nic, żadnej wskazówki. Dramat panowie. Odechciewa się.

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