[UWP] Verständnisproblem bei MVVM

  • WPF

Es gibt 13 Antworten in diesem Thema. Der letzte Beitrag () ist von Nofear23m.

    [UWP] Verständnisproblem bei MVVM

    Hey,
    ich versuche mich gerade an einer UWP App mit Einhaltung des MVVM Patterns. Das ist mein Ziel: Auf der MainPage soll eine ListBox mit Farben sein (zu Anfang leer). Jede Farbe hat einen Farbwert und einen BlendFactor. Über Slider in der ListBox lässt sich der BlendFactor einstellen. Die Hintergrundfarbe der Page soll jetzt die "Farbmischung" aus allen Farben in der Liste sein. Mit einem Button soll man zur nächsten Page kommen um eine Farbe aus einer Liste auszuwählen, die zur Liste in der MainPage hinzugefügt werden soll.

    Hier ist mein Model:
    Spoiler anzeigen

    C#-Quellcode

    1. ​ [TypeConverter(typeof(PaintTypeConverter))]
    2. public class Paint
    3. {
    4. public float BlendFactor { get; set; } = 1.0f;
    5. public Vector3 Value { get; set; }
    6. public string Name { get; set; }
    7. public Paint() { }
    8. public Paint(Color c, string name)
    9. {
    10. Name = name;
    11. Value = new Vector3(1.0f - c.R / 255.0f, 1.0f - c.G / 255.0f, 1.0f - c.B / 255.0f);
    12. }
    13. public Paint(Vector3 value)
    14. {
    15. Value = value;
    16. }
    17. public Color ToColor()
    18. {
    19. return Color.FromRgb((byte)((1.0f - Value.X) * 255.0f), (byte)((1.0f - Value.Y) * 255.0f), (byte)((1.0f - Value.Z) * 255.0f));
    20. }
    21. public static Paint BlendPaint(IEnumerable<Paint> paints)
    22. {
    23. Vector3 temp = new Vector3(0);
    24. foreach (Paint p in paints)
    25. temp += p.Value * p.BlendFactor;
    26. float max = Math.Max(temp.X, Math.Max(temp.Y, temp.Z));
    27. if(max > 1)
    28. temp /= max;
    29. return new Paint(temp);
    30. }
    31. }

    Viewmodel:
    Spoiler anzeigen

    C#-Quellcode

    1. public class PaintVm : INotifyPropertyChanged
    2. {
    3. public event PropertyChangedEventHandler PropertyChanged;
    4. protected void OnPropertyChanged(string name)
    5. {
    6. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    7. }
    8. public Paint Paint { get; set; } = new Paint();
    9. public PaintVm() { }
    10. public PaintVm(Paint p) => Paint = p;
    11. public float BlendFactor
    12. {
    13. get => Paint.BlendFactor;
    14. set
    15. {
    16. if (value == Paint.BlendFactor)
    17. return;
    18. Paint.BlendFactor = value;
    19. OnPropertyChanged("BlendFactor");
    20. }
    21. }
    22. public Vector3 Value
    23. {
    24. get => Paint.Value;
    25. set
    26. {
    27. if (value == Paint.Value)
    28. return;
    29. Paint.Value = value;
    30. OnPropertyChanged("Value");
    31. }
    32. }
    33. public string Name
    34. {
    35. get => Paint.Name;
    36. set
    37. {
    38. if (Paint.Name == value)
    39. return;
    40. Paint.Name = value;
    41. OnPropertyChanged("Name");
    42. }
    43. }
    44. public Brush DisplayBrush
    45. {
    46. get => new SolidColorBrush(Paint.ToColor());
    47. }
    48. }

    Spoiler anzeigen

    C#-Quellcode

    1. public class PaintCollectionVm : INotifyPropertyChanged
    2. {
    3. public event PropertyChangedEventHandler PropertyChanged;
    4. protected void OnPropertyChanged(string name)
    5. {
    6. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    7. }
    8. public ObservableCollection<PaintVm> Paints { get; set; } = new ObservableCollection<PaintVm>();
    9. public PaintCollectionVm()
    10. {
    11. }
    12. }

    Und XAML soweit:
    Spoiler anzeigen

    XML-Quellcode

    1. ​<Page
    2. x:Class="App1.MainPage"
    3. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    4. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    5. xmlns:local="clr-namespace:MVVMTest"
    6. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    7. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    8. mc:Ignorable="d"
    9. Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    10. <Page.DataContext>
    11. <local:PaintCollectionVm>
    12. <local:PaintCollectionVm.Paints>
    13. <local:PaintVm Paint="#826644, RawUmber"/>
    14. <local:PaintVm Paint="#8a3324, BurntUmber"/>
    15. </local:PaintCollectionVm.Paints>
    16. </local:PaintCollectionVm>
    17. </Page.DataContext>
    18. <Grid>
    19. <ListBox ItemsSource="{Binding Paints}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
    20. <ListBox.ItemTemplate>
    21. <DataTemplate>
    22. <Grid>
    23. <Grid.ColumnDefinitions>
    24. <ColumnDefinition Width="20"/>
    25. <ColumnDefinition Width="*"/>
    26. </Grid.ColumnDefinitions>
    27. <Border BorderBrush="Black" Background="{Binding DisplayBrush}" BorderThickness="1" Height="20"/>
    28. <TextBlock Text="{Binding Name}" Grid.Column="1" Margin="5"/>
    29. </Grid>
    30. </DataTemplate>
    31. </ListBox.ItemTemplate>
    32. </ListBox>
    33. </Grid>
    34. </Page>

    Nun zu den Fragen:
    Wie würde ich nach dem MVVM Pattern zu einer neuen Page navigieren, um mir in dieser eine Farbe auszuwählen und die dann zum PaintCollectionVm hinzuzufügen? Würde ich ein Command im Viewmodel nutzen hätte ich ja den Verweis aufs View im Viewmodel. Im Grunde ist es ja ein Dialog, nur wie designt man soetwas MVVM like?
    In der MainPage hätte ich ja ein ähnliches XAML mit dem Unterschied, dass noch ein Slider, der an BlendFactor gebunden ist, im DataTemplate der Listbox ist. Nun müsste ich bei jeder Änderung von BlendFactor (und wenn eine Farbe der Liste hinzugefügt wird) die Hintergrundfarbe der Page neu berechnen (Paint.BlendPaint()). Wie stell ich das am Besten an?

    Grüße
    Hallo

    Gonger96 schrieb:

    Würde ich ein Command im Viewmodel nutzen hätte ich ja den Verweis aufs View im Viewmodel. Im Grunde ist es ja ein Dialog, nur wie designt man soetwas MVVM like?

    Doch, schon über einen Command, nur das man eine Schnittstelle bemüht. ein Service eigendlich.

    Schau mal hier. Ich denke das ist ganz gut erklärt.
    In C# habe ich sonst nichts fertiges zur Hand - nur in VB.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Nofear23m“ ()

    Ups, haha. 8o . Habs editiert, Sorry.

    Grüße
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Ah, soweit. Also das hinzufügen mit dem Dialog und löschen klappt sehr gut. Habe es so wie beschrieben über ein IDialogService Interface gemacht. Allerdings steht im Codebehind meines Dialogs noch dies

    C#-Quellcode

    1. private void Button_Click(object sender, RoutedEventArgs e)
    2. {
    3. DialogResult = true;
    4. Close();
    5. }

    Für den Ok Button. Sieht mir nicht sehr MVVM mäßig aus. Deswegen, wie lässt sich so ein Ok-Button bzw. Abbrechen-Button besser gestalten?
    Bin zwar am Handy aber...

    In XAML setze beim Button IsDefault für den Button der True zurückgeben soll und IsCancel für den der Fals zurückgeben soll.

    Probier das bitte mal.
    Aber .Net 4.6 geht das aber vieleicht nicht mehr.

    Ich mach das über das Interface. Kann ich aber gerade nicht Posten.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Man man UWP ist doch anders als WPF :D Ich habe jetzt ein IDialogService und einen PaintDialogService der das Interface implementiert. Für Dialoge gibt's da den ContentDialog. Somit ist das Problem auch gelöst. (Blöd ist dass man keine TypeConverter implementieren kann).

    Damit bleibt nur noch meine letzte Frage: Wenn sich durch die Slider in der ListBox ein BlendFactor im PaintVm ändert, muss ich die DisplayBrush Property im PaintCollectionVm neu berechnen. Wie stell ich das an?
    Hallo

    Gonger96 schrieb:

    Wenn sich durch die Slider in der ListBox ein BlendFactor im PaintVm ändert, muss ich die DisplayBrush Property im PaintCollectionVm neu berechnen.

    Meinst du das die View sich DisplayBrush neu abrufen soll damit der Getter durchlaufen wird?
    Dann im Setter von BlendFactor am ende noch mit OnPropertyChanged("DisplayBrush")

    Das Animmiert die View DisplayBrush neu abzurufen. und der Wert wird im View aktualisiert.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Aja, sorry, irgendwie total überlesen.

    Das einfachste wäre dann wohl eine Methode wie RefreshView oder sowas in PaintCollectionVm welche dann das OnNotifyPropertyChanged aufruft.
    Und dann im Setter von BlendFactor einfach RefreshView. Dazu musst du aber die Instanz z.b. im Konstructor von PaintVm mitgeben.

    Also im Konstruktor von PaintVm die PaintCollectionVm mitgeben und in eine ReadOnly Variable halten.
    Und dann im Setter von BlendFactor _parentCollection.RefreshView().

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Sehr gut, läuft alles. Hier nochmal die aktuellen ViewModels
    Spoiler anzeigen

    C#-Quellcode

    1. public class PaintVm : INotifyPropertyChanged
    2. {
    3. public event PropertyChangedEventHandler PropertyChanged;
    4. public PaintCollectionVm PaintCollection { get; set; }
    5. protected void OnPropertyChanged(string name)
    6. {
    7. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    8. }
    9. public Paint Paint { get; set; } = new Paint();
    10. public PaintVm() { }
    11. public PaintVm(Paint p) => Paint = p;
    12. public PaintVm(string name, uint value)
    13. {
    14. Paint = new Paint(value, name);
    15. }
    16. public float BlendFactor
    17. {
    18. get => Paint.BlendFactor;
    19. set
    20. {
    21. if (value == Paint.BlendFactor)
    22. return;
    23. Paint.BlendFactor = value;
    24. OnPropertyChanged("BlendFactor");
    25. PaintCollection?.RefreshView();
    26. }
    27. }
    28. public Vector3 Value
    29. {
    30. get => Paint.Value;
    31. set
    32. {
    33. if (value == Paint.Value)
    34. return;
    35. Paint.Value = value;
    36. OnPropertyChanged("Value");
    37. }
    38. }
    39. public string Name
    40. {
    41. get => Paint.Name;
    42. set
    43. {
    44. if (Paint.Name == value)
    45. return;
    46. Paint.Name = value;
    47. OnPropertyChanged("Name");
    48. }
    49. }
    50. public Brush DisplayBrush => new SolidColorBrush(Paint.ToColor());
    51. }

    Spoiler anzeigen

    C#-Quellcode

    1. public class PaintCollectionVm : INotifyPropertyChanged
    2. {
    3. public event PropertyChangedEventHandler PropertyChanged;
    4. protected void OnPropertyChanged(string name)
    5. {
    6. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    7. }
    8. public PaintCollectionVm() { }
    9. private PaintVm selectedPaint;
    10. public PaintVm SelectedPaint
    11. {
    12. get => selectedPaint;
    13. set
    14. {
    15. selectedPaint = value;
    16. OnPropertyChanged("SelectedPaint");
    17. }
    18. }
    19. private Brush displayBrush;
    20. public Brush DisplayBrush
    21. {
    22. get => displayBrush;
    23. set
    24. {
    25. displayBrush = value;
    26. OnPropertyChanged("DisplayBrush");
    27. }
    28. }
    29. public ObservableCollection<PaintVm> Paints { get; set; } = new ObservableCollection<PaintVm>();
    30. public IDialogService<PaintVm> AddPaintService { get; set; }
    31. public ICommand AddPaintCommand => new RelayCommandAsync(AddPaint);
    32. public ICommand RemovePaintCommand => new RelayCommand(RemovePaint);
    33. public ICommand ClearPaintCommand => new RelayCommand(() => { Paints.Clear(); RefreshView(); });
    34. private async Task AddPaint()
    35. {
    36. var result = await AddPaintService.ShowDialog();
    37. if (result != null)
    38. {
    39. result.PaintCollection = this;
    40. Paints.Add(result);
    41. RefreshView();
    42. }
    43. }
    44. private void RemovePaint()
    45. {
    46. if (SelectedPaint != null)
    47. {
    48. Paints.Remove(SelectedPaint);
    49. RefreshView();
    50. }
    51. }
    52. public void RefreshView() => Blend();
    53. private void Blend()
    54. {
    55. Vector3 temp = new Vector3(0);
    56. foreach (PaintVm p in Paints)
    57. temp += p.Value * p.BlendFactor / 100.0f;
    58. float max = Math.Max(temp.X, Math.Max(temp.Y, temp.Z));
    59. if (max > 1)
    60. temp /= max;
    61. DisplayBrush = new SolidColorBrush(new Paint(temp).ToColor());
    62. }
    63. }

    Und XAML
    Spoiler anzeigen

    XML-Quellcode

    1. ​<Page
    2. x:Class="ColourMixer.MainPage"
    3. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    4. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    5. xmlns:local="using:ColourMixer"
    6. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    7. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    8. mc:Ignorable="d">
    9. <Page.DataContext>
    10. <local:PaintCollectionVm>
    11. <local:PaintCollectionVm.AddPaintService>
    12. <local:PaintDialogService/>
    13. </local:PaintCollectionVm.AddPaintService>
    14. </local:PaintCollectionVm>
    15. </Page.DataContext>
    16. <Grid>
    17. <ListBox Background="{Binding DisplayBrush}" ItemsSource="{Binding Paints}" SelectedItem="{Binding SelectedPaint, Mode=TwoWay}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
    18. <ListBox.ItemTemplate>
    19. <DataTemplate>
    20. <Grid>
    21. <Grid.ColumnDefinitions>
    22. <ColumnDefinition Width="20"/>
    23. <ColumnDefinition Width="150"/>
    24. <ColumnDefinition Width="200"/>
    25. </Grid.ColumnDefinitions>
    26. <Border Grid.Column="0" BorderBrush="Black" Background="{Binding DisplayBrush}" BorderThickness="1" Height="20"/>
    27. <TextBlock Grid.Column="1" Text="{Binding Name}" Margin="5" VerticalAlignment="Center"/>
    28. <Slider Grid.Column="2" Minimum="0" Maximum="100" Value="{Binding BlendFactor, Mode=TwoWay}" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Stretch"/>
    29. </Grid>
    30. </DataTemplate>
    31. </ListBox.ItemTemplate>
    32. </ListBox>
    33. <AppBar VerticalAlignment="Bottom">
    34. <StackPanel Orientation="Horizontal">
    35. <AppBarButton Icon="Add" VerticalAlignment="Center" Command="{Binding AddPaintCommand}"/>
    36. <AppBarButton Icon="Remove" VerticalAlignment="Center" Command="{Binding RemovePaintCommand}"/>
    37. <AppBarButton Icon="Clear" VerticalAlignment="Center" Command="{Binding ClearPaintCommand}"/>
    38. </StackPanel>
    39. </AppBar>
    40. </Grid>
    41. </Page>

    Ich bitte um Kritik falls noch irgendwas ungut ist.
    Ne, ich finde das sieht sauber aus. :rolleyes: Zumindest für mein Verständnis.

    Gonger96 schrieb:

    Ich bitte um Kritik falls noch irgendwas ungut ist.

    Das einzige was mir ins Auge fällt ist INotifyPropertyChanged. Nix großartiges, aber da du fragst... 8|

    Ich weis nicht wieviele ViewModels du hast/haben wirst, aber ich würde eine Basisklasse machen von welcher alle ViewModels erben.
    Diese implementiert das interface und folgende Methode:

    C#-Quellcode

    1. private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    2. {
    3. PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    4. }


    Der Vorteil der Verwendung von [CallerMemberName] ist zum einen das du den Propertynamen nicht immer schreiben musst sondern nur NotifyPropertyChanged() und der Name des Properties wird automatisch übergeben. Dennoch kannst du den Namen explizit angeben. z.b. wenn sich ein Property ändert welches nach sich zieht das z.b. ein ReadOnly Property nun "nicht mehr gültig ist" und aktualisiert werden muss.
    Dies würde ich dann allerdings mit NameOf ansprechen. NotifyPropertyChanged(NameOf(meinProperty))

    Der große Vorteil: Compilerprüfung und Refactoringmöglichkeit. Ändere ich den Namen des Property und nutze die "Rename" Funktion von VS wird dies auch dort nachgeführt.

    Freut mich das es klappt.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Ok, na dann nicht, das ist richtig. ;)
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##