Binding. Dwie klasy, dwie kontrolki.

0

Witam.

Stawiam pierwsze kroczki w WPF i nie bardzo mogę zrozumieć DataContext.

Co powinienem zrobić, zakładając że chce:

  1. Wrzucić na formę dla komponenty Label
  2. Stworzyć dwie niezależne klasy np:
    class Person
    {
        public string Name { get; set; }

        public Person(string name)
        {
            Name = name;
        }       
    }

    class Adress
    {
        public string PostCode { get; set; }

        public Adress(string postCode)
        {
            PostCode = postCode;
        }        
    }
  1. Stworzyć w klasie głównej obiekty powyższych klas., np
Person p = new Person("Mateo");
Adress a = new Adress("00-001");
  1. Stworzyć binding tak aby Label1 prezentował to co jest we właściwości name obiektu p, a Label2 to co jest we właściwości PostCode obiektu a.

Z tego co znalazłem w sieci w przypadku jednego obiektu można go użyć jako DataContext i później wykorzystać w bindingu, ale nie bardzo mogę pojąć jak to zrobić w tym wypadku...

Z góry dziękuję za pomoc.


EDIT:

Udało mi się to zrobić nadając właściwość Name do poszczególnych Labeli i przypisując DataContext do poszczególnych kontrolek.

Jednak z tego co czytałem konieczność wstawienia Name do kontrolki oznacza, że prawdopodobnie coś robimy nie tak...

Czy w tym przypadku jest to prawidłowe rozwiązanie?

1

Musisz mieć kontrolkę, która zgrupuje ci te dwa labele - DataContext jest dziedziczony, więc jeżeli go nadasz rodzicowi (np. całemu oknu) to wszystkie kontrolki wewnątrz również go będą miały.

0

DataContext służy do wiązania danych. Jest to jakby wskaźnik na klasę z której chcesz wyciągać jakieś dane. Możesz użyć bindingu aby aktualizować dane w klasie, kiedy właściwość kontrolki się zmienia (BindingMode = OneWayToSource) lub aktualizować dane w kontrolce kiedy właściwość w klasie sie zmienia (BindingMode = OneWay) lub oba naraz (BindingMode = TwoWay).

Dobrą cechą jest to, że każda kontrolka-dziecko będzie dziedziczyć DataContext po rodzicach.

Ale tu nachodzi nas myśl: Kiedy kontrolka wie, że ma się zaktualizować? DataContext korzysta z interfejsu INotifyPropertyChanged. Za pomocą eventu w nim zawartego możemy poinformować wszystkie obiekty obserwujące naszą właściwość o tym, że wartość jaką reprezentuje zmieniła się.

Na początku stwórzmy sobie projekt MainWindow.xaml, MainWindow.xaml.cs.
Dodajmy teraz pustą klasę MainWindowView. ta klasa będzie reprezentowała nasz DataContext i z niej będziemy tworzyć wszystkie właściwości do Bindingu.
screenshot-20180704015424.png

Mamy więc naszą klasę do której będziemy bindować. Zwróć uwagę, na przestrzeń nazw w jakiej znajduje się Twój MainWindowView

namespace testowa
{
    class MainWindowView
    {
    }
}

Kolejnym krokiem będzie zastąpienie obecnej wartości DataContext na naszą klasę MainWindowView. Otwórz MainWindow.xaml. Jeżeli twoja klasa znajduje się w innej przestrzeni nazw niż kontrolka możesz dodać nową przestrzeń nazw w ten sposób:

xmlns:NowyTagDlaPrzestrzeniNazw="clr-namespace:testowa.MynameSpace"

Gdy już wiemy, że kod XAML będzie widział naszą klasę ustawmy wartość dla DataContext:

<Grid>
        <Grid.DataContext>
            <local:MainWindowView x:Name="hMainWindowView"/>
        </Grid.DataContext>
    </Grid>

Przy okazji dodajmy od razu 2 TextBoxy:

<StackPanel>
            <TextBox />
            <TextBox />
</StackPanel>

Teraz chcielibyśmy zrobić tak:
Pierwszy **TextBox **będzie zapisywał we właściwości to, co do niego wpiszemy.
Drugi **TextBox **będzie odczytywał zmiany we właściwości.

Zabierzmy się więc do utworzenia takowej właściwości: Do klasy **MainWindowView **zaimplementuj interfejs INotifyPropertyChanged, za pomocą którego będziesz informował o zmianie wartości właściwości:
screenshot-20180704021506.png

następnie utwórzmy właściwość typu string:
csharp public string Tekst;
I tu powstaje problem, ponieważ kiedy właściwość się zmienia musimy powiadomić o tej zmianie metodą OnPropertyChanged().
Rozwiązanie:

  1. Stworzymy właściwość "text" przechowującą wartość +
  2. Stworzymy właściwość "Text" za pomocą której będziemy odczytywać i zapisywać do poprzedniej właściwości
        private string tekst;
        public string Tekst
        {
            get
            {
                return tekst;
            }
            set
            {
                tekst = value;
            }
        }

jedyne co nam teraz pozostało to użycie metody z interfejsu** INotifyPropertyChanged **od razu po tym kiedy właściwość zmieniła wartość (pamiętaj, że wartość zmienia się w naszej drugiej właściwości, pierwsza ją tylko przechowuje)

csharp OnPropertyChanged("Tekst");

teraz możemy zbindować **Property ** do naszego DataContext. Składnia jest następująca:

<TextBox Text="{Binding Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>

Efekt jest następujący:
screenshot-20180704023346.png

Ale my przecież chcemy użyć właściwości z tej klasy, a nie tej klasy. Aby odwołać się do właściwości musimy użyć ścieżki tzn:

<TextBox Text="{Binding Path=Tekst, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>

Od teraz kiedy zmieni się wartość właściwości "Tekst" zmieni się właściwość "Text" w kontrolce

Teraz kiedy wiesz jak to działa możemy zająć się twoim problemem:

  1. Stwórz swoje klasy w MainWindowView jako właściwości
  2. Pamiętaj, że Twoje klasy też muszą implementować interfejs INotifyPropertyChanged
  3. Aby odnosić się do elementów klasy spójrz na ten przykład i odezwij się jak będziesz miał jakiś problem

MainWindoowView:

public class MainWindowView : INotifyPropertyChanged
    {
        public MainWindowView()
        {
            MyDog = new Dog("pupilek");
            MyCat = new Cat("kiciuś");
        }

        #region ProperyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string PropertyName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
        }
        #endregion

        private Dog myDog;
        public Dog MyDog
        {
            get
            {
                return myDog;
            }
            private set
            {
                myDog = value;
                OnPropertyChanged("MyDog");
            }
        }

        private Cat myCat;
        public Cat MyCat
        {
            get
            {
                return myCat;
            }
            private set
            {
                myCat = value;
                OnPropertyChanged("MyCat");
            }
        }
    }

    public class Animal : INotifyPropertyChanged
    {
        public Animal(string Name)
        {
            this.Name = Name;
        }

        #region ProperyChanged
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string PropertyName)
        {
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
        }
        #endregion

        private string name;
        public string Name
        {
            get
            {
                return name;
            }
            private set
            {
                name = value;
                OnPropertyChanged("Name");
            }
        }
    }
    public class Dog : Animal, INotifyPropertyChanged
    {
        public Dog(string Name) : base(Name)
        {
            
        }
    }
    public class Cat : Animal, INotifyPropertyChanged
    {
        public Cat(string Name) : base(Name)
        {

        }
    }

Kod MainWindow.xaml

<TextBox Text="{Binding Path=MyDog.Name, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding Path=MyCat.Name, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>

Jeżeli coś nie jasne pytaj

2

@gswidwa: dlaczego klasę reprezentującą viewmodel nazywasz MainWindowView?

0

Ale przecież tutaj dzieją się jakieś niestworzone rzeczy. Najlepiej zrobić to prostym sposobem czyli po prostu przekazać odpowiednie obiekty do własności w VM i sprawa będzie załatwiona.

//BindableBase
    public class BindableBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChange(string name)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }
//MainWindowViewModel oraz reszta klas
    class Person
    {
        public string Name { get; set; }

        public Person(string name)
        {
            Name = name;
        }
    }

    class Address
    {
        public string PostCode { get; set; }

        public Address(string postCode)
        {
            PostCode = postCode;
        }
    }

    public class MainWindowViewModel : BindableBase
    {
        public string PersonName
        {
            get => person.Name;
            set
            {
                person.Name = value;
                RaisePropertyChange(nameof(PersonName));
            }
        }

        public string PostalCode
        {
            get => address.PostCode;
            set
            {
                address.PostCode = value;
                RaisePropertyChange(nameof(PostalCode));
            }
        }

        private Person person;
        private Address address;

        public MainWindowViewModel()
        {
            person = new Person("Grzegorz");
            address = new Address("99-111");
        }
    }
//code-behind
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var vm = new MainWindowViewModel();
            DataContext = vm;
        }
    }

@gswidwa implementowanie klas przedstawionych przez autora wątku jako viewmodele jest kompletnie bez sensu. W ogóle łamiesz z premedytacją DRY implementując za każdym razem interfejs miast zrzucić go do jakiejś klasy i po niej dziedziczyć.

Aha, jeszcze XAML:

<Window
    x:Class="App4P.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:local="clr-namespace:App4P"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    SizeToContent="WidthAndHeight"
    mc:Ignorable="d">
    <StackPanel>
        <Label Content="{Binding PersonName, UpdateSourceTrigger=PropertyChanged}" />
        <Label Content="{Binding PostalCode, UpdateSourceTrigger=PropertyChanged}" />
    </StackPanel>
</Window>

Rzecz jasna zawsze można utworzyć UserControl, przypiąć do niego ww viewmodel i wrzucić to jako kontrolka do okna.

0

Czasami się boję tu wchodzić i o coś pytać bo wychodzi tyle błędów wtedy w mojej aplikacji i złych przyzwyczajeń, że znów swoją apkę będę pisac chyba od nowa xD ( i tak od 6 lat)

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