Sposób na tworzenie obiektu ze zmieniającymi się dynamicznie parametrami

0

Witam,

Mam taki najprostszy przykład że przy naciśnięciu przycisku tworzę obiekt, zmieniam wartości zmiennych które są przekazywane jako parametry do konstruktora tego obiektu. Kod:

WPF:

<Window x:Class="TestWPF.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:TestWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Grid>
        <Rectangle x:Name="prostokat" HorizontalAlignment="Left" Height="292" Margin="407,10,0,0"
                   Stroke="Black" VerticalAlignment="Top" Width="18">
            <Rectangle.Fill>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="Black" Offset="{Binding jeden}"/>
                    <GradientStop Color="Black" Offset="{Binding dwa}"/>
                    <GradientStop Color="White" Offset="{Binding trzy}"/>
                </LinearGradientBrush>
            </Rectangle.Fill>
        </Rectangle>

        <Button Content="Button" HorizontalAlignment="Left" Margin="206,94,0,0"
                VerticalAlignment="Top" Width="75" Click="Button_Click"/>

    </Grid>
</Window>

C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;


namespace TestWPF {
    /// <summary>
    /// Logika interakcji dla klasy MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {


        public MainWindow() {

            InitializeComponent();

            Punkty punkty = new Punkty(0, cnt1, cnt2);

            prostokat.DataContext = punkty;
        }

        double cnt1 = 0.97;
        double cnt2 = 1;


        private void Button_Click(object sender, RoutedEventArgs e) {

            cnt1 -= 0.05;
            cnt2 -= 0.05;

            Punkty punkty = new Punkty(0, cnt1, cnt2);

            prostokat.DataContext = punkty;
        }

    }

    public class Punkty {

        public int jeden { get; set; }
        public double dwa { get; set; }
        public double trzy { get; set; }

        public Punkty(int jeden = 0, double dwa = 0.99, double trzy = 1) {

            this.jeden = jeden;
            this.dwa = dwa;
            this.trzy = trzy;
        }
    }
}

Program działa. Jednak chodzi o to że muszę stworzyć dwie dodatkowe zmienne cnt1, cnt2 przechowujące wartości żeby obiekt nie tworzył się od nowa taki sam, a stworzył się już ze zmienionymi wartościami (pasek szedł w górę). I mam pytanie czy ja muszę tworzyć te zmienne? Nie ma jakiegoś innego sposobu? Wolałbym aby działo się to w zakresie klasy bez potrzeby tworzenia dodatkowych pomocniczych zmiennych.

Pozdrawiam

1

Można na przykład w taki sposób, ale polecam zainteresować się interfejsem INotifyPropertyChanged.

Code-Behind

using System.Windows;

namespace TestWPF
{
	/// <summary>
	/// Interaction logic for MainWindow.xaml
	/// </summary>
	public partial class MainWindow : Window
	{
		public MainWindow()
		{
			InitializeComponent();
			var punkty = new Punkty(0, 0.97, 1);
			prostokat.DataContext = punkty;
		}

		private void Button_Click(object sender, RoutedEventArgs e)
		{
			var punkty = prostokat.DataContext as Punkty;
			punkty?.ZmienWartosci();
			prostokat.DataContext = null;
			prostokat.DataContext = punkty;
		}

	}

	public class Punkty
	{

		public int Jeden { get; set; }
		public double Dwa { get; set; }
		public double Trzy { get; set; }

		public Punkty(int jeden = 0, double dwa = 0.99, double trzy = 1)
		{
			Jeden = jeden;
			Dwa = dwa;
			Trzy = trzy;
		}

		public void ZmienWartosci()
		{
			Dwa -= 0.05;
			Trzy -= 0.05;
		}
	}
}

XAML

<Window x:Class="TestWPF.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:TestWPF"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
	<Grid>
		<Rectangle x:Name="prostokat" HorizontalAlignment="Left" Height="292" Margin="407,10,0,0"
                   Stroke="Black" VerticalAlignment="Top" Width="18">
			<Rectangle.Fill>
				<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
					<GradientStop Color="Black" Offset="{Binding Jeden}"/>
					<GradientStop Color="Black" Offset="{Binding Dwa}"/>
					<GradientStop Color="White" Offset="{Binding Trzy}"/>
				</LinearGradientBrush>
			</Rectangle.Fill>
		</Rectangle>

		<Button Content="Button" HorizontalAlignment="Left" Margin="206,94,0,0"
                VerticalAlignment="Top" Width="75" Click="Button_Click"/>

	</Grid>
</Window>
2
  1. Stwórz klasę zawierającą punkty.i zaimplementuj interfejs INotifyPropertyChanged. Służy on do powiadamiania kontrolek, że jakaś wartość się zmieniła. Poczytaj o tym w necie.
public class myPoint : INotifyPropertyChanged
{
      #region Property Changed
        public event PropertyChangedEventHandler PropertyChanged;
        public void OnPropertyChanged(string propertyName)
        {
            var handler = this.PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
     #endregion
     private double _x;
     public double x { get { return _x; } set { _x = value; OnPropertyChanged("x"); }
     private double _y;
     public double y { get { return _y; } set { _y = value; OnPropertyChanged("y"); }
}

Gdy już masz taką klasę możesz ją przypisać do danego DataContextu kontrolki:

         <Rectangle>
            <Rectangle.DataContext>
                <local:myPoint x:Name="PointData"/>
            </Rectangle.DataContext>
        </Rectangle>

Następnie możesz bindować property z własnościami w utworzonej klasie, a jeżeli chcesz coś zmienić odwołujesz się do "PointData".

3

No podpowiedziałeś niby dobrze ale trochę niechlujnie. :)

  1. Zamiast implementować interfejs INotifyPropertyChanged w każdej klasie z osobna najlepiej zrobić sobie klasę nadrzędną ViewModelBase, która ten interfejs będzie implementowała. Następnie po niej będą dziedziczyć wszystkie viewmodele. Oszczędzi to zaśmiecania kodu każdorazowym implementowaniem interfejsu tam gdzie trzeba i dodatkowo będziesz ładnie trzymał się zasady DRY;

  2. Samą implementację metody OnPropertyChanged można znacznie uprościć korzystając z dobrodziejstw składni C#, zamiast pisać tradycyjne ify. Zresztą IDE podpowiada gdzie tylko może, żeby stosować uproszczenia:

public void OnPropertyChanged(string property)
{
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
  1. Do wywołań samej metody OnPropertyChanged przekazujesz magic-stringi. O zgrozo! Nie rób tak, bo jak zmieni Ci się nazwa obiektu to będziesz musiał ręcznie jechać także zmianę parametru metody. Stosuj nameof(Variable). W ogóle dla nowszych wersji .NET już nawet nie trzeba przekazywać tutaj nazwy obiektu. Wszystko robi się samo.

  2. Nazwy typów danych oraz składowych publicznych piszemy z wielkiej litery.

  3. Definiowanie DataContext'u na poziomie XAML'a ogranicza bardzo Twój viewmodel gdyż, jeżeli nie korzystasz z frameworków, które potrafią same w tle podpiąć DataContext, nie będziesz mógł wykorzystać wewnątrz viewmodelu innego konstruktora niż domyślny. Wierz mi, jest to straszne ograniczenie i lepiej się tego wystrzegać. Najlepiej podpinać Context wewnątrz code-behind, bo wtedy można korzystać z konstruktora jakiego tylko chcesz.

  4. Ostatnia porada, może i najważniejsza. Najlepiej zainteresować się frameworkami, które dostarczają wsparcie dla MVVM out of box. Polecam naprawdę zaznajomić się z Prism'em gdyż:

  • Masz tam gotową klasę BindableBase, po której dziedziczą viewmodele,
  • Masz EventAggregator'a, który pozwala komunikować się klasom, w ogóle ze sobą nie powiązanym, o zdarzeniach, które samemu definiujesz. Bez tego narzędzia już pratycznie nie umiem pisać dobrych aplikacji mvvm, bo wierz mi... Do dobrego człowiek od razu się przyzwyczaja. Gdybym miał porzucić aggragatora na rzecz klasycznych rozwiązań to chyba bym się zastrzelił. ;)
  • Gotowe klasy implementujące ICommand takie jak chociażby DelegateCommand,
  • Wyzwalane na żądanie CanExecute dla komend zamiast ciągle wykonywane w pętli aplikacji, co czasami wręcz zabija wydajność,
  • Automatyczne bindowanie DataContextu do XAML'a,
  • W template'ach gotowe domyślnie ustawione kontenery IoC, które przydają się gdy chcesz wykorzystać sparametryzowany konstruktor viewmodelu, a powiem Ci, że prawie zawsze będziesz chciał z takowego skorzystać.
  • SetProperty praktycznie eliminujące potrzebę wywoływania OnPropertyChanged, które jeżeli jednak masz potrzebę to w Prismie także jest.

I wiele innych ficzerów, które bez takich frameworków musiałbyś napisać samodzielnie.

Pzdr. :)

0

Okej już między czasie ogarnąłem ten interfejs INotifyPropertyChanged w najprostszy sposób i jest OK, bardzo fajna rzecz oto mi chodziło. Człowiek ciągle uczy się czegoś nowego. Wielkie dzięki za pomoc, w szczególności grzesiek51114 za wyczerpującą odpowiedź ;)

0

@gswidwa, @Miccaldo: no dobrze, macie tutaj ode mnie przykład użycia Prisma, taki w sumie najbardziej podstawowy.

Projekt pokazuje jak robi się komunikację pomiędzy viewmodelami w taki sposób, żeby te viewmodele jednocześnie nic o sobie nie wiedziały. Macie tam dwie kontrolki UserControl. W lewej jest textbox z którego przekazujecie wiadomości do prawej kontrolki, która te wiadomości agreguje w listboksie.

Macie tam stosowne zachowania

  • Lewa kontrolka przysyła wiadomość do prawej;
  • Prawa kontrolka informuje lewą i ilości wiadomości na liście;
  • Lewa odbiera informacje o ilości wiadomości i jeżeli wynosi ona zero to nie będzie można dokonać czyszczenia listy (no, bo po co skoro nie ma tam wiadomości).

Macie tam też wykorzystanie kontenera IoC Unity od M$ (do wstrzykiwania event aggregatora do konstruktora viewmodeli) oraz wyzwalanie CanExecute na żądanie za pomocą RaiseCanExecuteChanged(). Zauważcie, że DataContext podpinany jest automatycznie pod warunkiem, że klasa viewmodelu nazywa się tak samo jak jej widok ale z dopiskiem ViewModel i opcją prism:ViewModelLocator.AutoWireViewModel ustawioną w XAML na True.

Powodzenia! :)

PS: Pisane na szybko więc wybaczcie jakieś dziwne referencje, bo czasami mi się Formsy niechcący dodawały ale usuwałem je :)

PrismExample.zip

0
            get => loaded ?? (loaded = new DelegateCommand(() =>
            {
                SendMessageEventToken = eventAggregator.GetEvent<SendMessageEvent>().Subscribe(payload =>
                {
                    Messages.Add(payload.Message);
                    eventAggregator.GetEvent<SendMessagesCountEvent>().Publish(Messages.Count);
                });

                ClearListOfMessagesToken = eventAggregator.GetEvent<ClearListOfMessagesEvent>().Subscribe(() => Messages.Clear());
            }));

To jest ten event aggregator. Będę to rozbijać jesczcze na czynniki pierwsze, ale głównie chodzi o to, że pobierasz listę "obserwatorów" i subskrybujesz zdarzenie?

 get =>
```Jak działa to? Nie wiem nawet jak wpisac w google ;P
2
gswidwa napisał(a):
 get =>
```Jak działa to? Nie wiem nawet jak wpisac w google ;P

To jest expresion-bodied member - nowy feature z C# 6
https://github.com/dotnet/roslyn/wiki/New-Language-Features-in-C%23-6#expression-bodied-function-members

1

Będę to rozbijać jesczcze na czynniki pierwsze, ale głównie chodzi o to, że pobierasz listę "obserwatorów" i subskrybujesz zdarzenie?

Zwyczajnie poprzez użycie Subscribe() stwierdzasz, że klasa ma być obserwatorem zdarzenia. Od tej pory będzie ona odbierać informacje o wszystkich publishach zdarzenia, niezależnie skąd będą one pochodziły, gdyż Event Aggregator zarejestrowany jest w IoC jako singleton.

get => xxx

To jest to samo co get { return xxx; }

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