Fermer

juillet 28, 2020

Création d'un CRM avec Xamarin.Forms et Azure, partie 2


Une fois que vous avez ajouté le serveur à votre liste de sources de packages, vous pouvez cliquer avec le bouton droit sur la solution et sélectionner "Gérer les packages NuGet pour la solution". Cela vous permettra d'installer un package sur plusieurs projets en même temps. Lorsque le gestionnaire de packages NuGet apparaît, procédez comme suit:

  1. Sélectionnez "Telerik" comme source du package, puis
  2. Filtrez par " Telerik.UI.for.Xamarin "
  3. Sélectionnez le Projets Xamarin.Forms
  4. Cliquez sur installer

 Figure2 "title =" Figure2 "data-openoriginalimageonclick =" true "/> </a></p>
<div id=

Vous êtes maintenant prêt à commencer à utiliser l'interface utilisateur pour les contrôles Xamarin!

Le NuGet l'installation du package est notre approche recommandée. Il est plus rapide de démarrer et grâce à la résolution des dépendances NuGet Package Manager, il installera les autres dépendances requises pour l'interface utilisateur pour Xamarin (par exemple, Xamarin SkiaSharp packages et requis Xamarin Packages de la bibliothèque de support Android ).

Option 2 – Références d'assembly installées

Si vous choisissez l'itinéraire de référence d'assembly, vous pouvez trouver les assemblys pour chaque projet dans des dossiers nommés dans le dossier d'installation de l'interface utilisateur Telerik pour Xamarin. Le chemin par défaut est: [19659011] C: Program Files (x86) Progress Telerik UI pour Xamarin [version] Binaries

Dans le dossier Binaries, vous trouverez des sous-dossiers correspondant aux types de projet. Chacun de ces dossiers contient les assemblages qui doivent être ajoutés au projet.

 Figure3 "title =" Figure3 "data-openoriginalimageonclick =" true "/> </a></p>
<div id=

Pick and Choose Assembly References [19659008] Si vous n'utilisez qu'un ou deux contrôles et que vous ne prévoyez pas d'utiliser l'éditeur de liens pour supprimer les assemblys inutilisés du package d'application compilé, vous pouvez sélectionner et choisir des assemblys individuels dans ces dossiers.

Pour connaître les assemblys requis sont pour chaque contrôle, consultez l'article du contrôle « Assemblages Telerik requis ». Vous constaterez que chaque contrôle a cet article sous le dossier «Mise en route». À titre d'exemple, voici l'emplacement de la RadListView Assemblages Telerik requis article:

 Figure4 "title =" Figure4 "data-openoriginalimageonclick =" true "/> </a></p>
<div id=

Afin que vous puissiez commencer à créer votre interface utilisateur dès que possible, je vous recommande de les ajouter tout au début pour que vous puissiez être opérationnel rapidement. Ensuite, une fois le projet terminé, vous pouvez revenir en arrière et supprimer les assemblys inutilisés si nécessaire.

Comme je l'ai mentionné dans la section Option 1 ci-dessus, il y a quelques dépendances. Si vous choisissez l'itinéraire de référence d'assembly, vous devrez ajouter explicitement ces packages NuGet. Passons en revue les deux maintenant.

Dépendance SkiaSharp

Plusieurs des interfaces utilisateur des contrôles Xamarin dépendent des bibliothèques SkiaSharp de Xamarin. Ajoutez les packages suivants aux projets:

  • SkiaSharp (v 1.68.0 pour tous les projets)
  • SkiaSharp.Views (v 1.68.0 pour tous les projets sauf la bibliothèque de classes )
  • SkiaSharp. Views.Forms (v 1.68.0 pour tous les projets)

Bibliothèques de support Android Xamarin

Les contrôles Android natifs ont besoin de quelques bibliothèques de support Android Xamarin pour fonctionner correctement. Accédez à l'article Bibliothèques de support Android requises pour voir la liste complète. Vous ne les installez que dans le projet Android et il doit s'agir de la même version du SDK Android.

 Figure5 "title =" Figure5 "data-openoriginalimageonclick =" true "/> </a></p>
<div id=

Maintenant que l'interface utilisateur Telerik pour les références de Xamarin a été ajoutée , passons au SDK Azure Mobile Service Client.

Azure Mobile Service Client SDK

L'une des principales raisons pour lesquelles nous avons commencé avec l'application de démarrage de téléchargement est que le package NuGet du SDK Microsoft Azure Mobile Client est déjà installé et a une classe TodoItemManager qui utilise le SDK pour interagir avec le back-end.

Ouvrez le fichier Constants.cs du projet de bibliothèque de classes et notez que l'URL du service sera déjà définie dans la propriété ApplicationURL, comme ceci:

public [19659033] statique classe Constantes

{

// Remplacez les chaînes par votre point de terminaison Azure Mobile App.

}

Il est maintenant temps de commencez à câbler le projet de bibliothèque de classes Xamarin.Forms pour pouvoir communiquer avec notre nouveau backend! La première chose que nous devons faire est de définir nos modèles.

Modèles de données

Notez le modèle de données TodoItem.cs fourni avec le projet. Avant de le supprimer, vous pouvez l'utiliser comme modèle pour les quatre classes de modèle de données dont vous avez besoin pour interagir avec le back-end:

 Figure6 "title =" Figure6 "data-openoriginalimageonclick =" true "/ > </a></p>
<p> Ces classes doivent avoir les mêmes propriétés que les modèles d'entité dans l'application serveur, mais avec quelques propriétés supplémentaires que je définis dans une classe de base partagée: </p>
<pre><code class=

Classes de service

Une fois les modèles définis, nous nous tournons maintenant vers la classe TodoItemManager. C'est le bourreau de travail qui synchronise les données entre Azure et l'application, mais gère également la synchronisation de la base de données hors connexion si vous l'avez activée.

Pour l'application Art Gallery CRM, nous avons besoin de quatre «classes de gestionnaire». Au lieu d'avoir plusieurs méthodes ou des surcharges compliquées, j'ai créé une interface qui définit ces opérations CRUD:

public interface IDataStore

{

Task < bool > AddItemAsync (T item);

Task < bool > UpdateItemAsync (élément T);

[19659008] Task < bool > DeleteItemAsync (élément T);

Tâche < bool > PurgeAsync ();

Tâche GetItemAsync ( string id);

Task < string > GetIdAsync ( string name);

Task <IReadOnlyList > GetItemsAsync ( bool forceRefresh = false );

Task < bool > PullLatestAsync ();

Task < bool > SyncAsync ();

}

Nous avons également besoin d'un moyen de partager une seule instance de MobileServiceClient, sinon nous aurions besoin d'avoir des instances séparées du client dans chaque classe de service, ce qui augmenterait inutilement le l'empreinte mémoire de l'application.

Une option que nous pouvons utiliser Il s'agit de placer la référence MobileServiceClient dans la classe App pour rendre la même instance disponible pour tous les services.

 Figure7 "title =" Figure7 "data-openoriginalimageonclick =" true "/> </a></p>
<p> Une fois l'interface définie et la référence MobileServiceClient disponible dans le monde entier, nous pouvons commencer à implémenter une classe de service DataStore pour chaque modèle d'entité. </p>
<p>À titre d'exemple, voici le DataStore de l'entité client. Dans le constructeur, nous définissons simplement la référence de la table CustomersTable et les méthodes requises par l'interface peuvent interagir avec cette référence. </p>
<p><a href= Figure8 "title =" Figure8 "data-openoriginalimageonclick =" true "/> </a></p>
<p> L'exemple ci-dessus montre la simplicité d'utilisation de MobileServiceClient pour interagir avec le service backend. Le SDK appellera le contrôleur et renverra les éléments sous la forme d'une liste d'éléments client fortement typée! </p>
<h3> Views and ViewModels </h3>
<p> Avec la couche de données prête , nous pouvons maintenant commencer à travailler sur l'interface utilisateur. L'application de démarrage a une seule page avec un ListView qui a été rempli avec une liste de TodoItems en utilisant une approche code-behind. Nous allons avoir besoin d'un moyen plus robuste de se déplacer dans l'application , ainsi que d'afficher et de modifier des données. </p>
<p> Nous pouvons utiliser un style à onglets ou une mise en page de style MasterDetail. J'ai décidé d'utiliser MasterDetail car il est plus logique d'avoir des pages de premier niveau dans MasterPage (le volet latéral ) et les pages détaillées (détails et edit) dans la DetailPage. </p>
<p> Les pages de premier niveau que je veux sont: </p>
<ul>
<li> Employés </li>
<li> Clients </li>
<li> Produits </li>
<li> Commandes </li>
<li> Expédition </li>
<li> Aide </li>
<li> À propos de </li>
</ul>
<p> Pour empêcher ce billet de blog de tourner dans un livre, je n'utiliserai que la table Employés pour démontrer et implémenter des concepts, des vues et des modèles de vue. </p>
<p> J'ignorerai également l'explication des concepts généraux de Xamarin.Forms lorsqu'il existe déjà un bon didacticiel dans la documentation de Xamairn qui Je peux fournir un lien pour. Le scénario Master-Detail est un exemple d'un tel concept. Vous pouvez trouver la configuration dans <a href= ce didacticiel Xamarin.Forms Master-Details .

À partir de ce point de l'article, supposons que l'infrastructure maître-détail est en place et que nous ajoutons des pages à cette configuration. Vous pouvez également référencer le code source de la démo sur GitHub pour suivre.

J'ai utilisé une convention de dénomination dans laquelle les modèles d'affichage de liste, de détail et d'édition utilisent le même nom que les pages. Cela permet d'assumer facilement la responsabilité des modèles de vue:

  • Employés Page et Employés ViewModel
  • EmployeeDetails Page & EmployeeDetail ViewModel
  • EmployeeEdit Page & EmployeeEdit ViewModel

Commençons à créer notre interface utilisateur!

EmployeesViewModel

J'aime parfois commencer avec le modèle de vue. que nous pouvons nous assurer que nous avons les propriétés dont nous avons besoin pour remplir la vue. C'est assez simple, une seule ObservableCollection pour contenir les éléments et une tâche pour charger les données dans la collection:

 Figure10 "title =" Figure10 "data-openoriginalimageonclick =" true "/> </a></p>
<div id=

Remarque: dans de nombreux endroits dans toute l'application, j'utilise un ObservableRangeCollection. Cela équivaut à un ObservableCollection, sauf que vous pouvez ajouter de nombreux éléments mais ne provoquer qu'une seule notification CollectionChanged, ce qui améliore les performances de liaison de données.

Voyons comment les éléments sont récupérés à partir d'Azure:

await DependencyService.Get <IDataStore > (). GetItemsAsync (forceRefresh);

Cela appellera la méthode sur la classe de service DataStore pour l'entité Employee. Tout le travail lourd est soigneusement emballé dans le SDK lui-même ou dans la classe de service et maintient le modèle de vue très lisible!

EmployeesPage

Avec le modèle de vue prêt, nous pouvons passer à l'interface utilisateur et à notre première interface utilisateur Telerik pour les contrôles Xamarin! Je ne veux pas seulement être un B le pour lister les employés, mais aussi les filtrer rapidement. Nous pouvons le faire avec une combinaison de l'interface utilisateur Telerik pour les contrôles Xamarin:

Voici le XAML réduit pour afficher la disposition de haut niveau:

 Figure11 "title =" Figure11 "data-openoriginalimageonclick =" true "/> [19659199] RadListView et RadEntry </h4>
<p> Avec le BindingContext de la page défini sur une instance de EmployeesViewModel, nous avons une propriété Employees qui contiendra la liste renvoyée par le backend. Nous l'utiliserons comme ItemsSource de RadListView: </p>
<pre><code class=

<dataControls: RadListView ItemsSource = "{Binding Employees}"

Style = "{StaticResource BaseListViewStyle}"

Grid.Row = "1">

Nous devons maintenant créer un ItemTemplate pour le RadListView afin d'afficher le Propriétés de l'employé dont nous avons besoin, le nom et la photo. Le BindingContext de l'objet ItemTemplate est l'objet employé individuel de la collection FilteredItems. Par conséquent, nous pouvons lier la propriété Text d'une étiquette à "Nom" et lier le contrôle So d'un contrôle Image urce à "PhotoUri".

Nous pouvons maintenant passer au filtrage en utilisant RadEntry et sa propriété Text:

<input: RadEntry x: Name = "FilterEntry"

Text = "{Binding SearchText, Mode = TwoWay} "

WatermarkText =" Search "

Margin =" 20,10,10,0 ">

Le texte est lié à la propriété SearchText de EmployeesViewModel, où la logique de filtrage est implémentée à l'aide du ApplyFilter méthode. Cette méthode exécute un filtrage personnalisé, puis met à jour la collection Employees afin que seuls les éléments filtrés soient présents et visualisés dans RadListView:

public string SearchText

{

get => _searchText;

set

{

SetProperty (ref _searchText, value);

this.ApplyFilter (value);

}

}

RadBusyIndicator

Regardons maintenant le RadBusyIndicator . Ce contrôle offre un bel ensemble d'animations personnalisées prêtes à l'emploi, mais vous pouvez également créer vos propres animations personnalisées si vous en avez besoin. Puisque cette application est destinée à un public professionnel, je suis allé avec Animation6 . Voir l'article de documentation sur les animations pour plus de détails, voici une liste des animations intégrées.

 Figure17 "title =" Figure17 "data-openoriginalimageonclick =" true "style =" vertical-align: middle; "/> </a></p>
<div id=

EmployeeDetailViewModel

Lorsque le modèle de vue est construit, nous avons la valeur de l'employé transmise en tant que paramètre de constructeur. Avec cela, nous définissons tout de suite SelectedEmployee:

public class EmployeeDetailViewModel: PageViewModelBase

{

public EmployeeDetailViewModel ( Employee item = null )

{

this .SelectedEmployee = item ;

...

}

public Employee SelectedEmployee

{

get => this [19659062] ._ selectedEmployee;

set => SetProperty ( ref this ._ selectedEmployee, value) ;

}

...

}

Je souhaite également générer des données sur cet employé pour remplir des graphiques, des jauges et d'autres fonctionnalités intéressantes de l'interface utilisateur. Pour soutenir cela, j'ai ajouté des collections pour contenir des points de données de graphique et d'autres données, ainsi qu'une tâche pour charger et calculer les données.

 Figure17_2 "title =" Figure17_2 "data-openoriginalimageonclick =" true "/> [19659007] Regardons de plus près <strong> LoadDataAsync </strong>c'est ici que je fais les calculs qui aboutissent aux valeurs utilisées pour le graphique et les jauges de la vue. </p>
<p> Il y a deux collections, une pour la compensation et une pour chiffre d'affaires. Nous pouvons calculer le premier en utilisant les valeurs de SelectedEmployee: </p>
<pre><code class=

La collection SalesHistory est une autre histoire, nous devons en fait extraire des éléments de la table Orders et trouver des correspondances pour cet employé Id:

// Obtient toutes les commandes de l'entreprise

var orders = await DependencyService.Get <IDataStore > (). GetItemsAsync ( true ) ;

// Définir les valeurs de l'entreprise [19659008] CompanySalesCount = orders.Count;

CompanySalesRevenue = Math.Floor (orders.Sum (o => o.TotalPrice));

// Récupère les commandes associées à l'employé

var employeeSales = orders . Où (o => o.EmployeeId == SelectedEmployee.Id) .OrderBy (o => o.OrderDate) .ToList ();

// Définir les valeurs des employés

EmployeeSalesCount = employeeSales.Count;

EmployeeSalesRevenue = Math.Floor (employeeSales.Sum (o => o.TotalPrice));

// Créer un graphique Historique des ventes data

foreach (var order in employeeSales)

{

SalesHistory.Add ( new ] ChartDataPoint

{

Value = order.TotalPrix ,

Date = order.OrderDate

}) ;

}

La logique du modèle de vue est terminée, il est maintenant temps de commencer à créer l'interface utilisateur.

Page Détails de l'employé

Pour le page de détails, je voulais un en-tête de style «héros» contenant la photo de l'employé et d'autres détails comme le nom et l'emplacement du bureau, tandis que dans le corps, nous pouvons avoir les graphiques et les jauges.

Voici un aperçu de haut niveau de cette mise en page: [19659345] Figure 18 "title =" Figure 18 "data-openoriginalimageonclick =" true "/>

Lorsque la méthode OnAppearing de la page est appelée, le modèle de vue LoadDataAsync est appelé car nous devons utiliser du code asynchrone. Cela rend l'application beaucoup plus réactive et rapide que si j'avais effectué tous les calculs à partir de la page, le constructeur de modèle de vue ou l'utilisation de .Wait () sur des méthodes asynchrones. Remarque: Vous devez éviter d'utiliser Attendre autant que possible!

Voici la méthode OnAppearing:

protected override async void OnAppearing ()

{

base .Apparaissant ();

wait vm.LoadDataAsync ();

SetupVacationGauge ();

SetupSalesGauge ();

SetupRevenueGauge ();

SetupChartLegend ();

}

Vous remarquerez que je configure une interface utilisateur ici aussi. La série de graphiques sera automatiquement mise à jour lorsque les ObservableCollections liées sont remplies, mais pour les jauges, je souhaite un contrôle un peu plus fin.

Charts

Commençons par les graphiques SalesHistory et Compensation. Ce sont simples. La série ItemsSources peut être liée directement aux collections de modèles de vues respectives:

LineChart

 Figure19 "title =" Figure19 "data-openoriginalimageonclick =" true "/> </a></p>
<p id= PieChart
 Figure20" title = "Figure20" data-openoriginalimageonclick = "true" /> </a></p>
<div id=

Jauges

Les jauges sont définies dans le XAML, mais sont configurées dans les méthodes clairement nommées que vous avez vues dans OnAppearing. Examinons de plus près Vacation RadialGauge comme exemple de l'une de ces configurations.

Voici le XAML, notez les x: Noms des composants de la jauge:

 Figure21 "title =" Figure21 "data-openoriginalimageonclick =" true "/> </a></p>
<div id=

Voici la méthode de configuration:

private void SetupVacationGauge ()

{

// Vacation Radial Gauge

VacationGauge.StartAngle = 90;

VacationGauge.SweepAngle = 360;

VacationGauge.AxisRadiusFactor = 1;

[19659008] VacationLinearAxis.Maximum = vm.SelectedEmployee.VacationBalance;

VacationLinearAxis.StrokeThickness = 0 020]

VacationLinearAxis.ShowLabels = false ;

VacationRangesDefinition.StartThickness = 10;

VacationRangesDefinition.EndThickness = 10;

VacationRangesDefinition.EndThickness = 10;

VacationRange.To = vm.SelectedEmployee.VacationBalance;

VacationRange.Color = (Color) Application.Current.Resources [ "GrayBackgroundColor" ];

[19659008] VacationIndicator.Value = vm.SelectedEmployee.VacationUsed;

VacationIndicator.Fill = (Color) A pplication.Current.Resources [ "AccentColor" ];

VacationIndicator.StartThickness = 10;

VacationIndicator.EndThickness = 10;

VacationIndicator.Start.Cap = GaugeBarIndicator;

VacationIndicator.EndCap = GaugeBarIndicatorCap.Oval;

}

} ] Le même modèle est également utilisé pour les autres jauges.

Suppression d'un employé

Également sur la page des détails, nous fournissons un moyen pour l'utilisateur de supprimer l'élément. Cela se fait à l'aide d'un événement de commande du bouton Supprimer de la barre d'outils.

Cependant, la suppression d'un employé a des conséquences car les enregistrements de la table Commandes peuvent contenir des commandes avec l'ID de cet employé. Je dois d'abord vérifier s'ils sont associés à des commandes ouvertes et avertir l'utilisateur que la suppression de l'employé supprimera également les commandes.

Voici l'action de la commande dans EmployeeDetailViewModel:

private async Task DeleteEmployeeAsync ()
{
if (this.SelectedEmployee == null)
{
return;
}

var result = await App.Current.MainPage.DisplayAlert ("Supprimer?", "Voulez-vous vraiment supprimer cet élément?", "Oui", "Non");

if (! Result)
{
return;
}

try
{
this.IsBusy = true;

// Do Referential Integrity Check
var orders = wait DependencyService.Get <IDataStore > (). GetItemsAsync ();
var matchingOrders = orders.Where (o => o.EmployeeId == this.SelectedEmployee.Id) .ToList ();

if (matchingOrders.Count> 0 )
{
var deleteAll = wait App.Current.MainPage.DisplayAlert ("!!! WARNING !!!", $ "Il y a des commandes dans la base de données pour {_vm? .SelectedEmployee .Nom}. If you delete this employee, you'll also delete all of the associated orders.rnnDo you wish to delete everything?", "Delete All", "Cancel");

            // Back out if user declines to delete associated orders
            if (!deleteAll)
                return;

            // Delete each associated Order
            foreach (var order in matchingOrders)
            {
                await DependencyService.Get<IDataStore>().DeleteItemAsync(order);
            }
        }

        // Lastly, delete the employee
        await DependencyService.Get<IDataStore>().DeleteItemAsync(this.SelectedEmployee);
    }
    catch (Exception ex)
    {
        // display error to user
    }
    finally
    {
        this.IsBusy = false;
    }

    await this.NavigateBackAsync();
}[19659446]

Note: There are other ways to handle this scenario. Deleting the related orders is a rather drastic measure. In a real-world app, you’re more likely to re-set the ID or use a nullable field for EmployeeID.

Editing an Employee

Now we’re ready for some editing! I could have built this so that the edit form is built into the details page. This is a common approach, however I wanted to also provide a way to add an employee with the same edit form.

I decided that a separate EmployeeEditPage would be better so I can keep the page code more readable and keep responsibilities separated.

The same EmployeeDetailsPage toolbar that has the delete button also has buttons for Edit and New Order. They can all share the same view model Command, but invoke different operations depending on a predefined command parameter.


    <ToolbarItem Text="order"
                 IconImageSource="add_order.png"
                 Command="{Binding ToolbarCommand}"
                 CommandParameter="order" />
    <ToolbarItem Text="edit"
                 IconImageSource="edit.png"
                 Command="{Binding ToolbarCommand}"
                 CommandParameter="edit" />
    <<ToolbarItem Text="delete"
                 Icon="delete.png"
                 Command="{Binding ToolbarCommand}"
                 CommandParameter="delete" />

Here's the view model command ToolbarItemTapped Action that will navigate to the EmployeeEditPagethe OrderEditPage or delete. We pass the SelectedEmployee to the constructor of both options.

private async void ToolbarItemTapped(object obj)
{
    if (this.SelectedEmployee == null)
    {
        return;
    }

    switch (obj.ToString())
    {
        case "order":
            await this.NavigateForwardAsync(new OrderEditPage(this.SelectedEmployee));
            break;
        case "edit":
            await this.NavigateForwardAsync(new EmployeeEditPage(this.SelectedEmployee));
            break;
        case "delete":
            await this.DeleteEmployeeAsync();
            break;
    }
}

Here’s the EmployeeDetailPage at runtime on Windows 10:

Figure22" title="Figure22" data-openoriginalimageonclick="true"/></a></p>
<div id=

EmployeeEditViewModel

This view model’s responsibility is simple:

  1. Determine if this is an existing employee or editing an existing employee
  2. Provide a mechanism to either insert a new or update an existing database record

To do this use, I the following items in the view model:

Let’s look at a shortened version of the UpdateDatabaseAsync method, notice that depending on the value of IsNewEmployeewe choose to either update or insert SelectedEmployee:

public async Task UpdateDatabaseAsync()

{

    if (IsNewEmployee)

    {

        await DependencyService.Get<IDataStore>().AddItemAsync(SelectedEmployee);

    }

    else

    {

        await DependencyService.Get<IDataStore>().UpdateItemAsync(SelectedEmployee);

    }

}

Note: Some code omitted for clarity, see demo source for full method.

So, how do we make the determination as to whether this is a new or existing employee? This is done using the page’s constructor.

EmployeeEditPage

The Edit page’s XAML is very simple:

Here’s the high-level layout:

Figure23" title="Figure23" data-openoriginalimageonclick="true"/></a></p>
<div id=

As you can see the DataForm’s Source is bound to the view model’s SelectedEmployee. The button’s click handler just assigns a random photo to the SelectedEmployee’s PhotoUri.

The extra interesting part of this page lies in the Employee model’s property attributes that tell the RadDataForm how to create its Editors and how the edit page determines if we’re editing or adding an employee.

Let’s start with the Employee model’s DataForm Annotations.

Data Annotations

The RadDataForm Data Annotations are model property attributes, which are used to set a wide range of DataForm features. You can find a list of the attributes in the RadDataForm documentation under the “Data Annotations” sub folder.

Figure24" title="Figure24" data-openoriginalimageonclick="true"/></a></p>
<div id=

For the employee model, I need to exclude the PhotoUri property because I want the button to set that value instead of a user-entered string. To do this, I can leverage the Ignore attribute:

[Ignore]

public string PhotoUri

{

    get => photoUri;

    set => SetProperty(ref photoUri, value);

}

The Data Annotations let you set the editor display and validation options as well. On the Employee model, I also use the DisplayOptionsNonEmptyValidator and NumericalRangeValidator attributes on the Employee properties:

Figure25" title="Figure25" data-openoriginalimageonclick="true"/></a></p>
<div id=

Page Constructor and Configuring Editors

As I mentioned at the beginning of this section, the page needs to decide if we’re editing an employee or creating a new one. This is simply achieved by using two constructors.

If the overloaded constructor is used, that means we passed an existing employee object to edit. If the normal constructor was used, then we’re creating a new employee:

// If we're adding a new employee

public EmployeeEditPage()

{

    InitializeComponent();

    ViewModel.IsNewEmployee = true;

    ViewModel.SelectedEmployee = new Employee();

 

    ConfigureEditors();

}

 

// If we're editing an existing employee

public EmployeeEditPage(Employee employee)

{

    InitializeComponent();

    ViewModel.SelectedEmployee = employee;

 

    ConfigureEditors();

}

In the ConfigureEditors method, we set the EditorType for each property. This tells the DataForm to use a specific Editor type, see the full list in the DataForm Editors documentation

private void ConfigureEditors()

{

    DataForm.RegisterEditor(nameof(Employee.Name), EditorType.TextEditor);

    DataForm.RegisterEditor(nameof(Employee.OfficeLocation), EditorType.TextEditor);

    DataForm.RegisterEditor(nameof(Employee.HireDate), EditorType.DateEditor);

    DataForm.RegisterEditor(nameof(Employee.Salary), EditorType.DecimalEditor);

    DataForm.RegisterEditor(nameof(Employee.VacationBalance), EditorType.IntegerEditor);

    DataForm.RegisterEditor(nameof(Employee.VacationUsed), EditorType.IntegerEditor);

    DataForm.RegisterEditor(nameof(Employee.Notes), EditorType.TextEditor);

}

We're almost done, the last remaining thing to cover is how the DataForm saves the data in its editors.

Saving

The DataForm has validation considerations, so the values in the editors are not immediately committed to the bound Source. Instead, you call DataForm.CommitAll and validation is performed against the values, (according to the Data Annotations you've set).

The DataForm has a FormValidationCompleted event that will fire when values are committed to the form. This means we can use an EventToCommandBehvaior to hook up the event to a view model command.

<input:RadDataForm x:Name="DataForm"
                   Source="{Binding SelectedEmployee}"
                   Grid.Row="1"
                   Margin="10,0,10,0">
   
        <behaviors:EventToCommandBehavior EventName="FormValidationCompleted"
                                          Command="{Binding FormValidationCompleteCommand}" />
   

In the command, we use the event args to hold a flag on whether or not the SelectedEmployee's changed can be saved.

private void FormValidationCompleted(object obj)
{
    if (obj is FormValidationCompletedEventArgs e)
    {
        this._isReadyToSave = e.IsValid;
    }
}

Just like the EmployeeDetailsPage, the EmployeeEditPage has a toolbar button that invokes a command. In the command's Action, we can save it and automatically navigate back to the details page when it is done.

private async void ToolbarItemTapped(object obj)
{
    switch (obj.ToString())
    {
        case "save":
            // Commit the Editor's values for validation
            this.DataFormView.CommitChanges();
            
            // If values pass validation
            if (this._isReadyToSave)
            {
                // Save the record
                if (await this.UpdateDatabaseAsync())
                {
                    await this.NavigateBackAsync();
                }
            }
            break;
    }
}

Wrapping Up

That concludes our deep dive in to the ListPage -> DetailsPage ->EditPage approach! The Art Gallery CRM app uses this pattern for all four of the entity models.

Now that you’ve got a handle on the pattern, take a look at the following special scenarios for some other ideas of what you could do:

  • RadDataGrid to easily group, sort and filter open Orders on the OrdersPage
  • RadSlideView for a great “out of the box experience” on the WelcomePage
  • RadListView with GridLayout to show beautiful product images on the ProductsPage
  • RadCalendar to show collated shipping dates on the CalendarPage
  • RadNumericInput and RadMaskedInput on the advanced OrderEditPage

In part three of this seriesI’ll show how to create and hook up an Azure Bot Service leveraging a custom trained Natural Language Understanding (LUIS) with the RadConversationalUI control to create an in-app Support Bot experience.

Read Next

⬅ Part One of the series where we built the backend
➡ Part Three of the series where we train a LUIS AI model using Telerik UI for Xamarin's Conversational UI 




Source link