Dies ist der zweite Artikel in einer Reihe, in der wir Kivy, Xamarin.Forms und React Native vergleichen. Darin werde ich versuchen, denselben Taskplaner zu schreiben, aber mit Xamarin.Forms. Ich werde sehen, wie ich es mache und was ich zu Gesicht bekommen muss.
Ich werde TK nicht wiederholen, es ist im ersten Artikel zu sehen:
Kivy. Xamarin Native reagieren. Drei Frameworks - ein ExperimentDer dritte Teil handelt von React Native:
Kivy. Xamarin Native reagieren. Drei Frameworks - ein Experiment (Teil 3)Zunächst möchte ich einige Worte zur Xamarin.Forms-Plattform und zur Herangehensweise an die Lösung der Aufgabe sagen. Xamarin.Forms ist ein Add-On für Xamarin.iOs und Xamarin.Android. Nach der Assembly wird der allgemeine Teil auf nativen Standardsteuerelementen „bereitgestellt“, sodass Sie im Wesentlichen vollständig native Anwendungen für alle unterstützten Plattformen erhalten.
Die Syntax von Xamarin.Forms kommt der Syntax von WPF sehr nahe, und der allgemeine Teil ist in .NET Standard geschrieben. Als Ergebnis erhalten Sie die Möglichkeit, den MVVM-Ansatz bei der Entwicklung der Anwendung zu verwenden und auf eine große Anzahl von Bibliotheken von Drittanbietern zuzugreifen, die für .NET Standard und bereits in NuGet geschrieben wurden und die Sie sicher in Ihren Xamarin.Forms-Anwendungen verwenden können.
Der Quellcode für die Anwendung hier ist auf
GitHub verfügbar.
Erstellen wir also eine leere Xamarin.Forms-Anwendung und legen los. Wir werden ein einfaches Datenmodell mit nur zwei Note- und Project-Klassen haben:
public class Note { public string UserIconPath { get; set; } public string UserName { get; set; } public DateTime EditTime { get; set; } public string Text { get; set; } } public class Project { public string Name { get; set; } public ObservableCollection<Note> Notes { get; set; } public Project() { Notes = new ObservableCollection<Note>(); } }
Ich werde versuchen, mich an den MVVM-Ansatz zu halten, aber ich werde keine spezielle Bibliothek verwenden, um den Code nicht zu komplizieren. Alle Modellklassen und Ansichtsmodelle implementieren die INotifyPropertyChanged-Schnittstelle. Ich werde die Implementierung in den angegebenen Codebeispielen der Kürze halber entfernen.
Der erste Bildschirm, den wir haben werden, ist eine Liste von Projekten mit der Möglichkeit, ein neues zu erstellen oder das aktuelle zu löschen. Machen wir ein Modell für ihn:
public class MainViewModel { public ObservableCollection<Project> Projects { get; set; } public MainViewModel() { Projects = Project.GetTestProjects(); } public void AddNewProject(string name) { Project project = new Project() { Name = name }; Projects.Add(project); } public void DeleteProject(Project project) { Projects.Remove(project); } }
Bildschirmcode selbst:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:TodoList.View" x:Class="TodoList.View.ProjectsPage"> <ContentPage.ToolbarItems> <ToolbarItem Clicked="AddNew_Clicked" Icon="plus.png"/> </ContentPage.ToolbarItems> <ListView ItemsSource="{Binding Projects}" ItemTapped="List_ItemTapped"> <ListView.ItemTemplate> <DataTemplate> <TextCell Text="{Binding Name}" TextColor="Black"> <TextCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/> </TextCell.ContextActions> </TextCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </ContentPage>
Das Markup erwies sich als recht einfach. Ich möchte nur auf die Implementierung von Wischschaltflächen zum Löschen von Projekten eingehen. In ListView gibt es das Konzept von ContextActions. Wenn Sie es festlegen, werden sie in iOS durch Wischen und in Android implementiert - durch langes Tippen. Dieser Ansatz ist in Xamarin.Forms implementiert, da er für jede Plattform nativ ist. Wenn wir jedoch in Android wischen möchten, müssen wir es mit unseren Händen im nativen Teil des Android implementieren. Ich habe keine Aufgabe, viel Zeit damit zu verbringen, also war ich mit dem Standardansatz zufrieden :) Als Ergebnis werden Swipe in iOS und das Kontextmenü in Android ganz einfach implementiert:
<TextCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/> </TextCell.ContextActions>
Durch Ersetzen der Testdaten erhalten wir die folgende Liste:

Fahren wir nun mit dem Ereignishandler fort. Beginnen wir mit einem einfachen - löschen Sie ein Projekt:
MainViewModel ViewModel { get { return BindingContext as MainViewModel; } } async Task DeleteItem_Clicked(object sender, EventArgs e) { MenuItem menuItem = sender as MenuItem; if (menuItem == null) return; Project project = menuItem.CommandParameter as Project; if (project == null) return; bool answer = await DisplayAlert("Are you sure?", string.Format("Would you like to remove the {0} project", project.Name), "Yes", "No"); if(answer) ViewModel.DeleteProject(project); }
Es ist nicht gut, etwas zu löschen, ohne den Benutzer zu fragen, und in Xamarin.Forms ist es einfach, dies mit der Standardmethode DisplayAlert zu tun. Nach dem Aufruf wird das folgende Fenster angezeigt:

Dieses Fenster stammt von iOs. Android wird eine eigene Version eines ähnlichen Fensters haben.
Als nächstes werden wir das Hinzufügen eines neuen Projekts implementieren. Es scheint, dass dies analog erfolgt, aber in Xamarin.Forms gibt es keine Implementierung eines Dialogs, der dem von mir bestätigten Löschen ähnelt, sondern die Eingabe von Text ermöglicht. Es gibt zwei mögliche Lösungen:
- Schreiben Sie Ihren eigenen Dienst, der native Dialoge auslöst.
- Implementieren Sie eine Problemumgehung auf der Seite von Xamarin.Forms.
Ich wollte keine Zeit damit verschwenden, den Dialog über den Muttersprachler zu führen, und entschied mich für den zweiten Ansatz, dessen Implementierung ich dem Thread
entnommen habe :
Wie erstelle ich einen einfachen InputBox-Dialog? , nämlich die Task InputBox-Methode (INavigation Navigation).
async Task AddNew_Clicked(object sender, EventArgs e) { string result = await InputBox(this.Navigation); if (result == null) return; ViewModel.AddNewProject(result); }
Jetzt werden wir tippenweise verarbeiten, um das Projekt zu öffnen:
void List_ItemTapped(object sender, Xamarin.Forms.ItemTappedEventArgs e) { Project project = e.Item as Project; if (project == null) return; this.Navigation.PushAsync(new NotesPage() { BindingContext = new ProjectViewModel(project) }); }
Wie Sie dem obigen Code entnehmen können, benötigen wir zum Anzeigen des Projektfensters das Ansichtsmodell und das Seitenobjekt des Fensters.
Ich möchte ein paar Worte zur Navigation sagen. Die Navigationseigenschaft ist in der VisualElement-Klasse definiert und ermöglicht es Ihnen, mit dem Navigationsbereich in jeder Ansicht Ihrer Anwendung zu arbeiten, ohne ihn dort mit den Händen rollen zu müssen. Damit dieser Ansatz funktioniert, müssen Sie dieses Panel jedoch noch selbst erstellen. Deshalb schreiben wir in App.xaml.cs:
NavigationPage navigation = new NavigationPage(); navigation.PushAsync(new View.ProjectsPage() { BindingContext = new MainViewModel() }); MainPage = navigation;
Wobei ProjectsPage genau das Fenster ist, das ich jetzt beschreibe.
Das Fenster mit Notizen ist dem Fenster mit Projekten sehr ähnlich, daher werde ich es nicht im Detail beschreiben, sondern mich nur auf interessante Nuancen konzentrieren.
Das Layout dieses Fensters erwies sich als komplizierter, da in jeder Zeile weitere Informationen angezeigt werden sollten:
Notizenansicht <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="TodoList.View.NotesPage" xmlns:local="clr-namespace:TodoList.View" xmlns:utils="clr-namespace:TodoList.Utils" Title="{Binding Project.Name}"> <ContentPage.Resources> <ResourceDictionary> <utils:PathToImageConverter x:Key="PathToImageConverter"/> </ResourceDictionary> </ContentPage.Resources> <ContentPage.ToolbarItems> <ToolbarItem Clicked="AddNew_Clicked" Icon="plus.png"/> </ContentPage.ToolbarItems> <ListView ItemsSource="{Binding Project.Notes}" x:Name="list" ItemTapped="List_ItemTapped" HasUnevenRows="True"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <local:MyCellGrid Margin="5"> <local:MyCellGrid.RowDefinitions> <RowDefinition Height="40"/> <RowDefinition Height="*"/> </local:MyCellGrid.RowDefinitions> <local:MyCellGrid.ColumnDefinitions> <ColumnDefinition Width="40"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="40"/> </local:MyCellGrid.ColumnDefinitions> <Image Grid.Row="0" Grid.Column="0" Source="{Binding UserIconPath, Converter={StaticResource PathToImageConverter}}" /> <StackLayout Grid.Row="0" Grid.Column="1"> <Label Text="{Binding UserName}" FontAttributes="Bold"/> <Label Text="{Binding EditTime}"/> </StackLayout> <Button Grid.Row="0" Grid.Column="2" BackgroundColor="Transparent" Image="menu.png" Margin="5" HorizontalOptions="FillAndExpand" Clicked="RowMenu_Clicked"/> <local:MyLabel Grid.Row="1" Grid.Column="1" Margin="0,10,0,0" Grid.ColumnSpan="2" Text="{Binding Text}"/> </local:MyCellGrid> <ViewCell.ContextActions> <MenuItem Clicked="DeleteItem_Clicked" IsDestructive="true" CommandParameter="{Binding .}" Text="Delete"/> </ViewCell.ContextActions> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </ContentPage>
Im Inhalt des Fensters haben wir wieder eine ListView, die an die Sammlung von Notizen angehängt ist. Wir möchten jedoch die Höhe der Zellen im Inhalt, jedoch nicht mehr als 150, dafür setzen wir HasUnevenRows = "True", damit die Zellen in der ListView so viel Platz beanspruchen können, wie sie benötigen. In dieser Situation können die Zeilen jedoch eine Höhe von mehr als 150 anfordern, und in der ListView können sie wie folgt angezeigt werden. Um dies in der Zelle zu vermeiden, habe ich meinen Erben des Grid-Panels verwendet: MyCellGrid. Dieses Feld für die Messoperation fordert die Höhe der internen Elemente an und gibt sie entweder 150 zurück, wenn sie größer ist:
public class MyCellGrid : Grid { protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { SizeRequest sizeRequest = base.OnMeasure(widthConstraint, heightConstraint); if (sizeRequest.Request.Height <= 150) return sizeRequest; return new SizeRequest(new Size() { Width = sizeRequest.Request.Width, Height = 150 }); } }
Da wir laut TOR in der Lage sein müssen, neben Tippen und Wischen auch das Menü zu bearbeiten und zu löschen, das durch Klicken auf die Schaltfläche in der Ecke der Zeile geöffnet wird, fügen wir diese Schaltfläche der Zellenvorlage hinzu und abonnieren das Tippen darauf. Wenn der Benutzer in diesem Fall auf die Schaltfläche klickt, fängt er die Geste ab und wir erhalten keine Ereignisse beim Klicken auf die Zeile.
<Button Grid.Row="0" Grid.Column="2" BackgroundColor="Transparent" Image="menu.png" Margin="5" HorizontalOptions="FillAndExpand" Clicked="RowMenu_Clicked"/>
Mit Testdaten sieht unser Formular folgendermaßen aus:

Die Verarbeitung von Benutzeraktionen in dieser Form entspricht vollständig der für das Projektlistenfenster geschriebenen. Ich möchte nur im Kontextmenü über unsere Schaltfläche in der Ecke der Zeile anhalten. Zuerst dachte ich, dass ich es auf der Xamarin.Forms-Ebene ohne Probleme tun würde.
In der Tat müssen wir nur eine Ansicht von so etwas erstellen:
<StackLayout> <Button Text=”Edit”/> <Button Text=”Delete”/> </StackLayout>
Und zeigen Sie es neben der Schaltfläche. Das Problem ist jedoch, dass wir nicht genau wissen können, wo es sich "neben der Schaltfläche" befindet. Dieses Kontextmenü sollte sich oben in der ListView befinden und beim Öffnen in den Koordinaten des Fensters positioniert sein. Dazu müssen Sie die Koordinaten der gedrückten Taste relativ zum Fenster kennen. Wir können die Koordinaten der Schaltfläche nur relativ zur internen ScrollView in der ListView abrufen. Wenn also die Linien nicht verschoben werden, ist alles in Ordnung, aber wenn die Linien gescrollt werden, müssen wir bei der Berechnung der Koordinaten berücksichtigen, wie viel gescrollt wurde. ListView gibt uns nicht den Bildlaufwert. Also muss es aus dem Eingeborenen herausgezogen werden, was ich wirklich nicht wollte. Aus diesem Grund habe ich mich für einen einfacheren und einfacheren Weg entschieden: Zeigen Sie das Standard-Systemkontextmenü an. Infolgedessen stellt der Button-Click-Handler Folgendes fest:
async Task RowMenu_Clicked(object sender, System.EventArgs e) { string action = await DisplayActionSheet("Note action:", "Cancel", null, "Edit", "Delete"); if (action == null) return; BindableObject bindableSender = sender as BindableObject; if(bindableSender != null) { Note note = bindableSender.BindingContext as Note; if (action == "Edit") { EditNote(note); } else if(action == "Delete") { await DeleteNote(note); } } }
Beim Aufrufen der DisplayActionSheet-Methode wird nur das reguläre Kontextmenü angezeigt:

Wenn Sie bemerken, wird der Text der Notiz in meinem MyLabel-Steuerelement und nicht im regulären Label angezeigt. Dies wird für was getan. Wenn der Benutzer den Text der Notiz ändert, werden Ordner ausgelöst und neuer Text kommt automatisch in Label an. Xamarin.Forms berechnet jedoch nicht gleichzeitig die Zellgröße neu. Xamarin-Entwickler behaupten, dass dies eine ziemlich teure Operation ist. Ja, und ListView selbst hat keine Methode, mit der es seine Größe wiedergeben könnte. InvalidateLayout hilft auch nicht. Das einzige, was sie dafür haben, ist die ForceUpdateSize-Methode des Cell-Objekts. Um zum richtigen Zeitpunkt dorthin zu gelangen und zu ziehen, habe ich meinen Label-Erben geschrieben und diese Methode für jede Textänderung angewendet:
public class MyLabel : Label { protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) { base.OnPropertyChanged(propertyName); if (propertyName == "Text") { ((this.Parent as MyCellGrid).Parent as Cell).ForceUpdateSize(); } } }
Nach dem Bearbeiten einer Notiz passt ListView die Zellengröße automatisch an den neuen Text an.
Beim Bearbeiten oder Erstellen einer neuen Notiz wird ein Fenster mit dem Editor im Inhalt und der Schaltfläche Speichern in der Symbolleiste geöffnet:

Dieses Fenster unterscheidet sich geringfügig von dem, was wir in der TK haben: das Fehlen eines runden Knopfes unten. Wenn Sie es einfach über dem Editor platzieren, wird es durch die verlassene Tastatur blockiert. Gleichzeitig fand ich keine schöne Lösung, um es zu bewegen und nicht mit einer schnellen Suche in den Eingeborenen zu gehen. Daher habe ich es entfernt und nur die Schaltfläche Speichern im oberen Bereich belassen. Dieses Fenster selbst ist sehr einfach, daher werde ich seine Beschreibung weglassen.
Was ich am Ende sagen möchte.
Xamarin.Forms eignet sich gut für diejenigen, die mit der .NET-Infrastruktur vertraut sind und schon lange damit arbeiten. Sie müssen nicht auf neue IDEs und Frameworks aktualisieren. Wie Sie sehen können, unterscheidet sich der Anwendungscode nicht wesentlich vom Code einer anderen XAML-basierten Anwendung. Darüber hinaus können Sie mit Xamarin iOS-Anwendungen in Visual Studio für Windows entwickeln und erstellen. Wenn Sie die endgültige Anwendung zum Testen und Erstellen entwickeln, müssen Sie eine Verbindung zu einem MacOS-Computer herstellen. Und Bibliotheken können ohne sie gemacht werden.
Um mit dem Schreiben von Anwendungen auf Xamarin.Forms zu beginnen, benötigen Sie keine roten Augen mit der Konsole. Installieren Sie einfach Visual Studio und schreiben Sie Anwendungen. Alles andere wurde bereits für Sie erledigt. Unabhängig davon, wie Microsoft mit kostenpflichtigen Produkten verbunden ist, ist Xamarin kostenlos und es gibt kostenlose Versionen von Visual Studio.
Die Tatsache, dass Xamarin.Forms den .NET-Standard unter der Haube verwendet, ermöglicht Ihnen den Zugriff auf eine Reihe bereits dafür geschriebener Bibliotheken, die Ihnen das Leben bei der Entwicklung Ihrer Anwendungen erleichtern.
Mit Xamarin.Forms können Sie ganz einfach etwas in die nativen Teile Ihrer Anwendung einfügen, wenn Sie etwas Plattformspezifisches implementieren möchten. Dort erhalten Sie das gleiche C #, aber die API ist für jede Plattform nativ.
Natürlich gab es einige Mängel.
Die im allgemeinen Teil verfügbare API ist eher dürftig, da sie nur das enthält, was allen Plattformen gemeinsam ist. Wie in meinem Beispiel zu sehen ist, enthalten beispielsweise alle Plattformen Warnmeldungen und Kontextmenüs, und diese Funktion ist in Xamarin.Forms verfügbar. Das Standardmenü, in das Sie Text eingeben können, ist jedoch nur in iOS verfügbar, in Xamarin.Forms hingegen nicht.
Ähnliche Einschränkungen bestehen bei der Verwendung von Komponenten. Etwas kann getan werden, etwas ist unmöglich. Das gleiche Wischen zum Löschen eines Projekts oder einer Notiz funktioniert nur unter iOS. In Android wird diese Kontextaktion als Menü angezeigt, das bei langem Tippen angezeigt wird. Und wenn Sie in den Android wischen möchten, dann willkommen im Android-Teil und schreiben Sie es mit Ihren Händen.
Und natürlich Leistung. Die Geschwindigkeit der Anwendung auf Xamarin.Forms ist in jedem Fall niedriger als die Geschwindigkeit der nativen Anwendung. Microsoft selbst sagt also, dass Xamarin.Forms genau das Richtige für Sie ist, wenn Sie eine Anwendung ohne Schnickschnack in Bezug auf Design und Leistung benötigen. Wenn Sie Hübschheit oder Geschwindigkeit brauchen, sollten Sie bereits zum Eingeborenen gehen. Glücklicherweise verfügt Xamarin auch über native Versionen, die bereits sofort mit ihren nativen Plattform-APIs arbeiten und schneller als Formulare arbeiten.