Laden...

Eingaben aus ContentViews an ViewModel übergeben

Letzter Beitrag vor 2 Jahren 12 Posts 892 Views
Eingaben aus ContentViews an ViewModel übergeben

Hallo zusammen,

ich habe mir eine ContentView mit Labels und einem Entry zur Eingabe:


<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             x:Class="MyApp.Pages.Views.MyControlView"
             x:Name="this">

    <StackLayout BindingContext="{x:Reference this}">
        <Grid Margin="20, 0, 20, 0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>


            <StackLayout Grid.Row="0" Grid.Column="0" VerticalOptions="Center">
                <Label Text="{Binding NameLabelString}" />
                <Label Text="{Binding IsOptionalLabelString}" FontSize="12" />
            </StackLayout>

            <StackLayout Grid.Row="0" Grid.Column="1" VerticalOptions="Center" >
                <Entry Text="{Binding TextInput}" Placeholder="{Binding PlaceholderString}" Keyboard="{Binding KeyboardSetting}" Margin="5, 0, 5, 15" />
            </StackLayout>

            <StackLayout Grid.Row="0" Grid.Column="2" VerticalOptions="Center" >
                <Label Text="{Binding BundeslandString}"/>
            </StackLayout>
        </Grid>

       <StackLayout Margin="20, 50, 15, 0">
                <Grid RowSpacing="10">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>

                    <Button Grid.Row="0" Grid.Column="0" Margin="5"
                            Text="OK"
                            Command="{Binding SaveItemCommand}" />

                    <Button Grid.Row="0" Grid.Column="1" Margin="5"
                            Text="Abbrechen"
                            Command="{Binding CancelItemItemCommand}" />
                </Grid>
            </StackLayout>

    </StackLayout>

</ContentView>

Elemente dieser ContentView werden dann in einer anderen ContentPage "instanziiert" bzw. verwendet:


<StackLayout>
            <controls:MyControlView NameLabelString="Name 1:"
                                    IsOptionalLabelString="Erforderlich"
                                    PlaceholderString="Platzhalter für Name 1"
                                    TextInput="{Binding TextInputString1}"/>

            <controls:MyControlView NameLabelString="Name 2:"
                                     IsOptionalLabelString="Optional"
                                     PlaceholderString="Platzhalter für Name 2"
                                     TextInput="{Binding TextInputString2}" />

            ...
</StackLayout>

Die ContentView hat in ihrer Code-Behind-Datei dann BindableProperties, so dass ich von außen zu jeder Verwendung auch Parameter innerhalb der ContentPage weitergeben kann:


public partial class MyControlView : ContentView
{
	public static readonly BindableProperty NameLabelProperty = BindableProperty.Create(nameof(NameLabelString), typeof(string), typeof(MyControlView), string.Empty);
        public static readonly BindableProperty IsOptionalProperty = BindableProperty.Create(nameof(IsOptionalLabelString), typeof(string), typeof(MyControlView), string.Empty);
        ...

        public static readonly BindableProperty TextInputProperty = BindableProperty.Create(nameof(TextInput), typeof(string), typeof(MyControlView), string.Empty);


        public string NameLabelString
         {
                get => (string)GetValue(NameLabelProperty); 
                set => SetValue(NameLabelProperty, value);
         }

        public string IsOptionalLabelString
        {
                get => (string)GetValue(IsOptionalProperty);
                set => SetValue(IsOptionalProperty, value);
         }

         ...

         public string TextInput
         {
                get => (string)GetValue(TextInputProperty);
                set => SetValue(TextInputProperty, value);
          }


         public MyControlView()
         {
                InitializeComponent();
         }
}

Da ich ja jetzt mehrere Instanzen dieses UserControls (also meiner MyControlView) habe, suche ich nach einem Weg, um die Werte aus dem Entry, das darin ist, in das ViewModel der ContentPage zu bringen. Im UI gibt es ja unter diesen MyControlView-Elementen ja noch einen "OK"-Button, der dann die eingegebenen Werte aus dem MyControlView in eine Datenbank speichern soll. Diese [RelayCommand]-Implementierung für das "SaveCommand" habe ich im ViewModel der ContentPage untergebracht. Dort müsste ich auch auf die Werte der Eingaben aus den MyControlView-Entrys zugreifen, um sie in die Datenbank zu bringen.

Ich hab mir gedacht, dass ich in der ContentPage, dort wo die Elemente der MyControlView eingebracht sind, ich dann jeweils Parameter der Entrys habe, die ich dann mittels weiterem DataBinding in das ViewModel der ContentPage bringe:


<StackLayout>
            <controls:MyControlView NameLabelString="Name:"                                                  
                                         IsOptionalLabelString="Erforderlich"
                                         PlaceholderString="Platzhalter Name 1"
                                         [b]TextInput="{Binding TextInputString1}[/b]"/>

           ...
</StackLayout>

Leider klappt das nicht:


public partial class MyContentPageViewModel : ObservableObject
{
        [ObservableProperty]
        private string textInputString1;

        ...

        [RelayCommand]
        async Task SaveItem()
        {
            // auswerten des TextInputString1, TextInputString2, ... und in die Datenbank speichern
        }

        [RelayCommand]
        async Task CancelItem()
        {
            // Just navigate to the previous page
            await Shell.Current.GoToAsync("..");
        }
    }

Ist das ein gängiger Weg, um die Daten aus den Entrys der ContentView in das ViewModel zu bringen, oder gibt es da bessere Mechanismen? Ich müsste im SaveItem eben noch eine Validation der Werte machen, so dass die erforderlichen Elemente auch mit Inputs versorgt sind etc.
Dann klappt das Ganze bei mir aber auch nicht, weil er das DataBinding über TextInput="{Binding TextInputString1}"/> nicht hinbekommt. Im TextInputString1 kommt schlicht weg nix an.

Wie würdet Ihr das Ganze realisieren?

Hast Du Mal folgendes versucht: TextInput="{Binding TextInputString1, Mode=TwoWay}"
Kann man über den defaultBindingMode Parameter bei der Definition der BindableProperty auch global einstellen.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Ja super, das scheint es tatsächlich gewesen zu sein! Ich hatte das mal auf einer der BindableProperties ausprobiert, aber da hatte es wohl nicht geholfen, da ich evtl. in einem anderen Scope war und er dann vermutlich nur zur ContentView synchronisiert hätte.

Bin ich generell mit dieser Art der Weitergabe bzw. des Bindings auf der richtigen Seite, oder sollte ich das besser machen? Im Grunde hat ja jede ContentView ihr eigenes Entry, was irgendwohin seinen Wert melden muss.

Gleichzeitig ist die Frage, ob das Validieren, das aus dem Command-Handler des OK-Button angetriggert wird, so gemacht wird, oder ob es da bessere Möglichkeiten gibt.
Im Grunde muss ich hier evaluieren, ob wirklich ein nicht optionales Eingabefeld auch einen Wert hat. So müsste ich ja wieder zurückgemeldet bekommen, ob dieses Feld auch wirklich optional oder mandatory war. Sind dann jeweils wieder eigene Properties, die über die ContentPage von der ContentView aus angebunden werden müssen, an das ViewModel der ContentPage.

Mit Scopes hat das eigentlich nichts zu tun, nur, ob Bindings standardmäßig in beide Richtungen gehen sollen oder nicht. Bei bestehenden Controls (z.B. Entry) wurde das natürlich sinnvoll eingestellt.

Bin ich generell mit dieser Art der Weitergabe bzw. des Bindings auf der richtigen Seite, oder sollte ich das besser machen?

Ich kenne dein gesamtes Vorhaben nicht, aber auf den ersten Blick würde ich das vermutlich ähnlich machen.

Im Grunde muss ich hier evaluieren, ob wirklich ein nicht optionales Eingabefeld auch einen Wert hat.

Mit MAUI habe ich bisher nur wenig gemacht, ich kann daher nur sagen, wie es bei WPF geht.
Und da gab's die ValidationRules, die direkt am Binding gesetzt werden und ein recht einfaches, aber auch eingeschränktes Konzept zur Validierung sind.

Ich habe das immer über das INotifyDataErrorInfo-Interface gemacht, das liefert Informationen, ob es Fehler gibt, welche Property welche Fehler hat und ein Event, wenn sich was ändert.
Das hat den Vorteil, dass Du eine generelle Basis hast, die Du beliebig flexibel aus dem ViewModel heraus steuern kannst.
WPF hat das verwendet und entsprechend angezeigt, ich gehe davon aus (hoffe), dass das bei MAUI auch geht, aber teste es aus.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Benutze lieber [relaycommand(CanExecute=CanCommand)] und mach die Validierung dadrin, dann geht der Button erst an, wenn Validierung OK war

  • Wer lesen kann, ist klar im Vorteil
  • Meistens sitzt der Fehler vorm Monitor
  • "Geht nicht" ist keine Fehlermeldung!
  • "Ich kann programmieren" != "Ich habe den Code bei Google gefunden"

GidF

Und ja, im CanExecute geht das natürlich auch.
Das hätte dann aber zur Folge, dass der Button deaktiviert wird, solange dabei false zurück kommt, das ist nicht immer gewollt.

Mein Favorit ist das INotifyDataErrorInfo-Interface, ich implementiere das als eigene Klasse, die ich dann in den verschiedenen VMs nutze und im Command (Execute oder CanExecute) prüfe ich ganz simpel, ob's Fehler gibt oder nicht.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Danke für die nützlichen Tipps und Links zum Validieren.
Meine Frage zielte ursprünglich bissl mehr auf das Vorgehen ab mit der foldengen Struktur

  • ContentPage --> deklariert mehrere ContentViews im XAML
  • ContentViews enthalten jeweils ein Entry für die eigentliche Eingabe der Werte
  • ContentView definiert am Entry das DataBinding auf eine Property im Code-Behind der ContentView (eigentlich nur dazu gedacht, von außen die ContentView zu konfigurieren mit dem PlaceHolder, KeyBoard-Setting etc.)
  • ContentPage definiert das DataBinding an jeder definierten ContentView auf einen String im ViewModel der ContentPage (... TextItemOne="{Binding TextItemOneString, Mode=TwoWay}"/>)
  • Ein Button auf der ContentPage hat ein RelayCommand mit Handler im ViewModel der ContentPage, das dann ggf. die Validation der TextItemOneString, TextItemTwoString, ... machen muss (oder eben über eine von Euch vorgeschlagene Validation-Methode)
  • Nach erfolgreicher Validation werden dann die von der ContentView heraus gereichten Entry-Werte aus dem ViewModel RelayCommand an den Datenbank-Service zum Speichern übergeben

Jetzt bin ich irgendwie etwas erschlagen von verschiedenen Möglichkeiten der Validierung.
Diese Variante hier wurde mir auch noch angezeigt https://learn.microsoft.com/en-us/dotnet/architecture/maui/validation (Api-Docs). Hier erscheint mir auf den ersten Blick die direkte Rückmeldung an das Entry-Control sehr vorteilhaft.

Beim Interface

INotifyDataErrorInfo

finde ich zwar recht schön das Einbinden der Evaluierung anhand der zu implementierenden Methoden, aber mir fehlt irgendwie im Kopf noch der Schritt, wie ich direkt das Feedback an das entsprechende Entry bringe? Ich denke, ein Icon an der Seite, im Entry selber oder eine rote Umrandung bzw. etwas in der Art müsste man aus Usability-Gründen schon machen und auch so, dass dieses Fehler-Feedback wieder verschwindet, wenn man den Fehler behoben hat (z.B. bei einem nicht-optionalen String, dass ein Text eingegeben wurde).

Ich weiß jetzt nicht, ob diese Art der Validierung aus dem Link von oben irgendwelche Nachteile hat bzw. auch warum es so viele verschiedene Wege hier gibt. Das verwirrt erst einmal etwas, muss ich zugeben.

Dein Link funktioniert nicht.
TextValidationBehavior - .NET MAUI Community Toolkit - .NET Community Toolkit
Kannst Du natürlich auch nutzen, wenn das für deine Zwecke ausreicht, spricht da nichts gegen, einfach ist ja nicht schlecht, wenn es die Anforderungen erfüllt.
Ich habe nur immer gerne das INotifyDataErrorInfo verwendet, da ich somit überall im Projekt das gleiche Validierungs-Konzept hatte, im ViewModel vor der konkreten Logik so oder so nochmal validiere und auf diese Weise weit mehr Möglichkeiten habe.

Und ob oder wie das INotifyDataErrorInfo in MAUI supportet wird, musst Du testen.
Bei WPF wurde das aber automatisch verwendet, dafür sind ja die drei Member, die informieren, ob es Fehler gibt, welche Property welche Fehler hat und ein Event, wenn sich was ändert.
Dazu gab's dann noch ein ErrorTemplate an den Controls, über das Du definieren kannst, wie der Fehler dargestellt werden soll - standardmäßig ein roter Rahmen.
Jedes ViewModel muss das dann implementieren (die konkrete Implementierung hatte ich ausgelagert, um nicht alles zu kopieren) und validiert bei jeder Text-Änderung oder z.B. erst beim Button-Klick.
Text falsch: Fehler der Liste hinzufügen, Text richtig: Fehler-Liste leeren

Aber ich finde so auf Anhieb nichts, zu MAUI und INotifyDataErrorInfo 😠
Kann also gut sein, dass der Tipp Müll ist, weil es nicht supportet wird.
Oder Du baust es dir selber, könnte aber etwas kompliziert werden, dass das auch überall funktioniert, ohne dass Du überall z.B. so ein Behavior anhängen musst, sollte aber gehen.
Ich arbeite in meiner Freizeit an einem Kleinen Test-Projekt, da werde ich das auf jeden Fall ausprobieren, aber das dauert noch - das Problem mit dieser (Frei)zeit ^^

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Was ich tatsächlich beim INotifyDataErrorInfo auch gut finde, ich die Validierung im ViewModel.
Mein Hauptproblem ist hier tatsächlich die Rückmeldung und Update der UI. Im App-Umfeld gibt es ja meist etwas mit einem Ausrufezeichen neben einem Entry oder einer roten Umrandung des Entry oder rote Schriftart. Zusätzlich müsste das Ganze ja dann auch wieder verschwinden, wenn man die Eingabe gemäß der Validierungs-Logik dann ja wieder richtig gemacht hat. Ein Save-Button darf dann während es noch Fehler gibt, auch erst einmal nix machen.

Aktuell gelingt mir insbesondere das Update der UI und Fehler-Rückmeldung an den User noch nicht. Es wirkt so auf mich, als ob das nicht das bevorzugte Konzept bei .NET MAUI ist (ohne das aber zu wissen). Zusätzlich benutze ich ja auch das Community Toolkit und es wirkt so, als ob in Summe dann mehrere Konzepte vermischt werden. Das fände ich dann nicht so gut

Der funktionierende Link von oben wäre dann der hier:
https://learn.microsoft.com/en-us/dotnet/architecture/maui/validation

Genau, dieses TextValidationBehavior von Dir ist auch noch so ein Weg, der für mich ein eigenes und anderes Konzept darstellt

Mein Hauptproblem ist hier tatsächlich die Rückmeldung und Update der UI. Im App-Umfeld gibt es ja meist etwas mit einem Ausrufezeichen neben einem Entry oder einer roten Umrandung des Entry oder rote Schriftart. Zusätzlich müsste das Ganze ja dann auch wieder verschwinden, wenn man die Eingabe gemäß der Validierungs-Logik dann ja wieder richtig gemacht hat.

Wie gesagt: Das lief automatisch.
Ob es ein Ausrufezeichen gab, weiß ich nicht mehr, kann man sich aber selber stylen.
Gucks dir doch bei WPF an, da funktioniert es auf jeden Fall.

Aktuell gelingt mir insbesondere das Update der UI und Fehler-Rückmeldung an den User noch nicht.

Ja, kann sein, dass es nicht unterstützt wird.

Hier das gleiche bei Xamarin.Forms, da wurde es im CodeBehind nachgebaut:
How to implement data validation with Xamarin.Forms | Packt Hub
Langfristig sicher keine gute Option, aber das könnte man adaptieren um eine wiederverwendbare Lösung zu entwickeln.

Der funktionierende Link von oben wäre dann der hier:

>

Oder Du arbeitest dich da durch, das kenne ich jetzt noch gar nicht.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.