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
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.
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
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.
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?
Jetzt habe ich den Fehler gefunden - ist mir fast nicht aufgefallen:
Das MVVM-Toolkit erzeugt ja Code für die als
[RelayCommand]
attributierten Command-Handling-Methoden und baut da als Suffix noch ein "..Command" mit dazu.
Heißt also, mein Command-Handler heißt hier nicht "DeleteItem", sondern "DeleteItemCommand".
Mein Code bei den SwipeItems müsste also wie folgt aussehen:
...
<SwipeItems>
<SwipeItem Text="Delete" BackgroundColor="Red"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:Tab2ViewViewModel}}, Path=DeleteItemCommand}"
CommandParameter="{Binding .}"/>
</SwipeItems>
...
So kommt er dann über die RelativeSource und den AncestorType mit dem entsprechenden Path auch auf den Command-Handler drauf.
PS: Was mich nur sehr verwirrt hat, ich sehe bei Project > Dependencies > net7.0-android > Analyzers > CommunityToolkit.Mvvm.SourceGenerators > CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator bzw. den anderen Unterkategorien von CommunityToolkit.Mvvm.SourceGenerators nicht immer die Implementierungen, sondern ein schwarzes Symbol und nix weiter. Woran kann das denn liegen?
Okay, denke ein wenig mehr Klarheit konnte ich in meine Umsetzung mit den Routen rein bringen und funktioniert jetzt über das Shell-Objekt direkt dann 🙂
...
await Shell.Current.GoToAsync(nameof(Tab2Details), true);
...
Hallo,
ich habe in einer ContentPage ein wenig mit DataBinding in einem SwipeItem experimentiert und wollte dort ein Delete-Command einrichten, was aber nicht in der Person-Klasse selbst liegt und über das DataTemplate referenziert wird, sondern in es liegt in meiner ViewModel-Klasse "Tab2ViewViewModel".
Über folgenden Code komme ich irgendwie nicht an mein "DeleteItem" ran und weiß auch nicht, wie das ich dem Command-Handler meine aktuell zu löschende Person als Parameter mitgeben kann. Die Person-Klasse liegt unter dem Namespace "MyApp.Models" und die Command-Handler sind in der Klasse "Tab2ViewViewModel", welche im Namespace MyApp.ViewModels liegt.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodel="clr-namespace:MyApp.ViewModels"
xmlns:models="clr-namespace:MyApp.Models"
x:DataType="viewmodel:Tab2ViewViewModel"
x:Class="MyApp.Pages.Tab2View"
Title="Tab 2 View">
<ContentPage.ToolbarItems>
<ToolbarItem IconImageSource="..." Command="{Binding AddNewItemCommand}" />
</ContentPage.ToolbarItems>
<Grid RowDefinitions="20, Auto" RowSpacing="10" Margin="20, 0, 20, 20">
<SearchBar Grid.Row="0" Grid.Column="0" Grid.RowSpan="5" Placeholder="Tab 2 Item search..." />
<CollectionView Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Margin="0, 20, 0, 0"
ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="{x:Type models:Person}">
<SwipeView>
<SwipeView.RightItems>
<SwipeItems>
<SwipeItem Text="Delete" BackgroundColor="Red"
Command="{Binding Source={RelativeSource AncestorType={x:Type viewmodel:Tab2ViewViewModel}}, Path=DeleteItem}"
CommandParameter="{Binding .}"/>
</SwipeItems>
</SwipeView.RightItems>
<Grid Padding="0, 5">
<Frame>
<Label Text="{Binding Name}" FontAttributes="Bold" />
</Frame>
</Grid>
</SwipeView>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</ContentPage>
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MyApp.Models;
using System.Collections.ObjectModel;
namespace MyApp.ViewModels
{
public partial class Tab2ViewViewModel : ObservableObject
{
private static int _personCounter = 1;
[ObservableProperty]
private ObservableCollection<Person> items;
[ObservableProperty]
private Person personItem;
public Tab2ViewViewModel()
{
items = new ObservableCollection<Person>();
}
[RelayCommand]
private void AddNewItemCommand()
{
personItem = new Person();
personItem.Name = $"Person {_personCounter++}";
items.Add(personItem);
}
[RelayCommand]
private void DeleteItem(Person item)
{
if (items.Contains(item))
{
items.Remove(item);
}
}
}
}
Okay, dann werde ich von dem ursprünglichen Vorhaben absehen. Denke, wenn das eher unüblich ist, macht es auch keinen Sinn.
Um ehrlich zu sein, hab ich die Navigation über AppShell dann noch nicht ganz verstanden. Die AppShell übernimmt bei mir die Anordnung in meine Tabs und die entsprechenden Seiten dazu:
<TabBar>
<Tab Title="Tab 1" Icon="...">
<ShellContent
ContentTemplate="{DataTemplate pages:Tab1View}"
Route="Tab1View"
Shell.PresentationMode="Animated" />
</Tab>
<Tab Title="Tab 2" Icon="...">
<ShellContent
ContentTemplate="{DataTemplate pages:Tab2View}"
Route="Tab2View"
Shell.PresentationMode="Animated" />
</Tab>
</TabBar>
Bei mir ist aber so, dass die Tab2View selber im XAML ein Icon in Form eines "+" bereitstellt und daran ein RelayCommand angebunden hat. Wenn der Command-Handler dazu aufgerufen wird, müsste ich erst die Tab2AddNewView instanziieren und laden.
Das würde ich jetzt gedanklich erst einmal nicht im XAML der AppShell.xml definieren, sondern wenn dann würde bei mir das Routing wie oben in der Tab2View.xml angeordnet werden. Aber ich müsste die ja dynamisch erzeugen und darstellen. Ist mir so noch nicht ganz klar, wie ich das mache.
Das von Bernd vorgeschlagene
Navigation.PushModalAsync(new Tab2AddNewView(), true);
kann ich nicht aufrufen. Es gibt bei mir wohl kein Navigation-Objekt und beim PushModalAsync(...) würde bei mir eher ein Tab2AddNewViewViewModel() gefragt werden. Da stimmt noch was nicht ganz...
Hallo zusammen,
ich bin relativ neu in der Entwicklung mit .NET MAUI und war etwas erstaunt über die angebotenen Arten von Pages zu denen man navigieren kann. Auf der Seite https://learn.microsoft.com/en-us/dotnet/maui/user-interface/pages/navigationpage?view=net-maui-7.0 werden hier eigentlich nur wenige Seiten angeboten.
An sich wollte ich von einer ContentPage, die ich über ein Tab aus der AppShell heraus anzeige, an einem "+" Zeichen innerhalb eines ToolBarItem eine Art "Add New"-Dialog einblenden. Vorgestellt hatte ich mir das so, dass dieser, ähnlich zu einer AlertMessage, nur so eingeblendet wird und der Hintergrund der bestehenden ContentPage wird ausgegraut, bis der "Add New"-Dialog geschlossen wird.
Leider tu ich mir mit den Pages aus dem Link oben etwas schwer. Hier würde ja nur die NavigationPage in Frage kommen, die dann das vorhandene Fenster komplett überdeckt, bis diese Page wieder vom Stack runter und geschlossen ist.
Ich will mich natürlich nicht gegen gängige Praxis bei der Steuerung und Navigation in Apps streuben, aber irgendwie weiß ich auch nicht, so ganz gefällt mir das nicht.
Außerdem ist mir auch noch nicht ganz klar, wie ich nach dem Einrichten des Commands auf dem "+"-Button dann auch die Page richtig anzeigen muss. Das Binding zum RelayCommand habe ich über das MVVM-Toolkit gemacht und die Methode zum Reagieren auf das Command dann in die entsprechende AddNewDialogViewModel-Klasse gemacht. Hoffe, das war richtig? Zudem war mir nicht ganz klar, ob ich hier besser ein AsyncRelayCommand nehmen sollte oder nicht. Finde eigentlich, dass es hier nicht unbedingt ein Async braucht, oder?
Wie würdet Ihr Obiges sinnigerweise umsetzen?