This example demonstrates how to access data protected by the Security System from a non-XAF Xamarin Forms application.
From this tutorial, you will learn how to perform CRUD (create-read-update-delete) operations with respect to security permissions.
You can find the complete project code in the sub-directories.
- Visual Studio 2019 with the Mobile development with .NET workload.
- (Optional) A paired Mac to build the application on iOS.
NOTE: If you have a pre-release version of our components, for example, provided with the hotfix, you also have a pre-release version of NuGet packages. These packages will not be restored automatically and you need to update them manually as described in the Updating Packages article using the Include prerelease option.
- Open Visual Studio and create a new project.
- Search for the Mobile App (Xamarin Forms) template.
- Specify the project name (we use "XamarinFormsDemo" in this demo project) and click Create.
- Select an application template (we use the Tabbed template in this demo project) and click OK.
Getting started with XamarinForms
The application you will build in this tutorial requires the following NuGet Packages:
- DevExpress.ExpressApp.Security.Xpo
- DevExpress.ExpressApp.Validation
- DevExpress.Persistent.BaseImpl
From Visual Studio's Tools menu, select NuGet Package Manager > Package Manager Console.
Make sure of the following:
- Package source is set to All or DevExpress XX.Y Local
- Default project is set to a class library project (XamarinFormsDemo in this example)
Run the following commands:
Install-Package DevExpress.ExpressApp.Security.Xpo
Install-Package DevExpress.ExpressApp.Validation
Install-Package DevExpress.Persistent.BaseImpl
To reuse existing data models and Security System settings (users, roles and permissions) stored in an XAF application database, add a reference to the [YourXafProject].Module.NetCore project. We use the following project in this tutorial: XafSolution.Module.NetCore. See also: Port an Existing XAF Application to .NET Core 3.0+.
To create a new data model, use XPO Data Model Wizard. In this scenario, you can re-use built-in classes (PermissionPolicyUser, PermissionPolicyRole) or create custom security objects. Refer to the following help topics for additional information:
- How to: Implement a Custom Security System User Based on an Existing Business Class.
- How to: Implement Custom Security Objects (Users, Roles, Operation Permissions).
A mobile application does not have direct access to server-based resources, such as a database. In this demo, we will use an intermediate Web API service to communicate with a remote database server. Add an ASP.NET Core WebApi application to your project and follow this tutorial: Transfer Data via REST API.
The static XpoHelper
class exposes the following members:
- The
CreateUnitOfWork
method - returns a new UnitOfWork instance connected to a secured Object Access Layer. - The
SecuritySystem
property - returns a SecurityStrategyComplex object. Use this object with the following extension methods to check user permissions: IsGrantedExtensions Methods.
- Remove the following line in the App.xaml.cs file:
// Remove this line: DependencyService.Register<MockDataStore>();
- Replace the IDataStore.cs and MockDataStore.cs files in the Services folder with the XpoHelper.cs file in a class library project.
using DevExpress.ExpressApp.Security; using DevExpress.Xpo; namespace XamarinFormsDemo.Services { public static class XpoHelper { static readonly SecurityStrategyComplex fSecurity; public static SecurityStrategyComplex Security { get { if(fSecurity == null) { throw new InvalidOperationException("The security system is not initialized. Call the InitSecuritySystem method first."); } return fSecurity; } } public static UnitOfWork CreateUnitOfWork() { throw new System.NotImplementedException(); } } }
- Add a static constructor and initialize XAF Security System.
using DevExpress.ExpressApp.Xpo; using DevExpress.ExpressApp; using XafSolution.Module.BusinessObjects; using DevExpress.Persistent.BaseImpl.PermissionPolicy; // ... static XpoHelper() { RegisterEntities(); fSecurity = InitSecuritySystem(); } static void RegisterEntities() { XpoTypesInfoHelper.GetXpoTypeInfoSource(); XafTypesInfo.Instance.RegisterEntity(typeof(Employee)); XafTypesInfo.Instance.RegisterEntity(typeof(PermissionPolicyUser)); XafTypesInfo.Instance.RegisterEntity(typeof(PermissionPolicyRole)); } static SecurityStrategyComplex InitSecuritySystem() { AuthenticationStandard authentication = new AuthenticationStandard(); var security = new SecurityStrategyComplex( typeof(PermissionPolicyUser), typeof(PermissionPolicyRole), authentication); security.RegisterXPOAdapterProviders(); return security; }
- Add the following lines to the static constructor to configure XAF Tracing System and disable configuration manager, which is not supported by Xamarin.
using DevExpress.Persistent.Base; // ... Tracing.UseConfigurationManager = false; Tracing.Initialize(3);
- To use a self-signed SSL certificate for development, add the following method and call it in the static constructor.
using DevExpress.Xpo.DB; using System; using DevExpress.Xpo.DB.Helpers; using System.Net.Http; // ... static XpoHelper() { //.. #if DEBUG ConfigureXpoForDevEnvironment(); #endif //.. } // ... static void ConfigureXpoForDevEnvironment() { XpoDefault.RegisterBonusProviders(); DataStoreBase.RegisterDataStoreProvider(WebApiDataStoreClient.XpoProviderTypeString, CreateWebApiDataStoreFromString); } static IDataStore CreateWebApiDataStoreFromString(string connectionString, AutoCreateOption autoCreateOption, out IDisposable[] objectsToDisposeOnDisconnect) { ConnectionStringParser parser = new ConnectionStringParser(connectionString); if(!parser.PartExists("uri")) throw new ArgumentException("The connection string does not contain the 'uri' part."); string uri = parser.GetPartByName("uri"); HttpClient client = new HttpClient(GetInsecureHandler()); client.BaseAddress = new Uri(uri); objectsToDisposeOnDisconnect = new IDisposable[] { client }; return new WebApiDataStoreClient(client, autoCreateOption); } /// <summary> /// Disables an SSL sertificate validation to support self-signed developer certificates. /// </summary> /// <returns></returns> static HttpClientHandler GetInsecureHandler() { HttpClientHandler handler = new HttpClientHandler(); handler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true; return handler; }
- Add the
GetObjectSpaceProvider
,Logon
, andLogoff
methods. Refer to the following article for information on how to specify a proper connection string: Transfer Data via REST APIusing DevExpress.ExpressApp.Security.ClientServer; // ... const string ConnectionString = @"XpoProvider=WebApi;uri=https://localhost:5001/xpo/"; static IObjectSpaceProvider ObjectSpaceProvider; static IObjectSpaceProvider GetObjectSpaceProvider() { if(ObjectSpaceProvider == null) { ObjectSpaceProvider = new SecuredObjectSpaceProvider(Security, ConnectionString, null); } return ObjectSpaceProvider; } public static void Logon(string userName, string password) { var logonParameters = new AuthenticationStandardLogonParameters(userName, password); Security.Authentication.SetLogonParameters(logonParameters); IObjectSpace logonObjectSpace = GetObjectSpaceProvider().CreateObjectSpace(); Security.Logon(logonObjectSpace); } public static void Logoff() { Security.Logoff(); }
- Implement the
CreateUnitOfWork
method.public static UnitOfWork CreateUnitOfWork() { var space = (XPObjectSpace)GetObjectSpaceProvider().CreateObjectSpace(); return (UnitOfWork)space.Session; }
Change the ViewModels\BaseViewModel.cs file as follows.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using DevExpress.Xpo;
using XamarinFormsDemo.Services;
namespace XamarinFormsDemo.ViewModels {
public class BaseViewModel : INotifyPropertyChanged {
bool isBusy = false;
public bool IsBusy {
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title {
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null) {
if(EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "") {
var changed = PropertyChanged;
if(changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
add new class to the ViewModels
folder, name it XpoViewModel
and change it accordingly:
using DevExpress.Xpo;
using Xamarin.Forms;
using XamarinFormsDemo.Services;
using XamarinFormsDemo.Views;
namespace XamarinFormsDemo.ViewModels {
public class XpoViewModel : BaseViewModel {
UnitOfWork unitOfWork;
protected UnitOfWork UnitOfWork {
get {
if(unitOfWork == null) {
unitOfWork = XpoHelper.CreateUnitOfWork();
}
return unitOfWork;
}
}
public XpoViewModel() {
if(!XpoHelper.Security.IsAuthenticated) {
App.ResetMainPage();
}
}
}
}
Make every other ViewModel, except LogIn
, inherit XpoViewModel
instead of BaseViewModel
.
-
In the ViewModels/LoginViewModel.cs file, add the
UserName
andPassword
properties, and change theOnLoginClicked
method.string userName; public string UserName { get { return userName; } set { SetProperty(ref userName, value); } } string password; public string Password { get { return password; } set { SetProperty(ref password, value); } } private async void OnLoginClicked(object obj) { try { XpoHelper.Logon(UserName, Password); await Shell.Current.GoToAsync($"{nameof(ItemsPage)}"); } catch(Exception ex) { await Shell.Current.DisplayAlert("Login failed", ex.Message, "Try again"); } }
-
In the Views/LoginPage.xaml file, add the
UserName
andPassword
fields.<ContentPage.Content> <Grid ColumnSpacing="20" Padding="15"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Label Text="Login" FontSize="Medium" Grid.Row="0" Grid.Column="0" /> <Entry Text="{Binding UserName}" FontSize="Small" Margin="0" Grid.Row="1" Grid.Column="0" /> <Label Text="Password" FontSize="Medium" Grid.Row="2" Grid.Column="0" /> <Entry Text="{Binding Password}" IsPassword="True" FontSize="Small" Margin="0" Grid.Row="3" Grid.Column="0" /> <Button Text="Log In" Command="{Binding LoginCommand}" BackgroundColor="#ff7200" TextColor="White" FontSize="Medium" Margin="0" Grid.Row="4" Grid.Column="0"/> </Grid> </ContentPage.Content>
-
In the
App.xaml.cs
change shell creation accordingly:-
add
ResetMainPage
methodpublic static Task ResetMainPage() { Current.MainPage = new AppShell(); return Shell.Current.GoToAsync("//LoginPage"); }
-
Call
ResetMainPage()
in theApp
class constructor instead ofMainPage = new AppShell();
-
In the
AppShell.xaml
file add routing parameter to theBrowse
shell content<ShellContent Title="Browse" Icon="icon_feed.png" Route="ItemsPage" ContentTemplate="{DataTemplate local:ItemsPage}" />
-
Change the ViewModels\ItemsViewModel.cs and ViewModels\ItemsPage.xaml files to implement a ListView with the list of items, a filter bar, and Toolbar items.
-
Create the properties and commands in the
ItemsViewModel
class.public ItemsViewModel() { Title = "Browse"; Items = new ObservableCollection<Employee>(); ExecuteLoadEmployeesCommand(); LoadDataCommand = new Command(() => { ExecuteLoadEmployeesCommand(); //.. }); //.. } ObservableCollection<Employee> items; public ObservableCollection<Employee> Items { get { return items; } set { SetProperty(ref items, value); } } public Command LoadDataCommand { get; set; } void ExecuteLoadEmployeesCommand() { if(IsBusy) return; IsBusy = true; LoadEmployees(); IsBusy = false; } public void LoadEmployees() { try { var items = UnitOfWork.Query<Employee>().OrderBy(i => i.FirstName).ToList(); Items = new ObservableCollection<Employee>(items); } catch(Exception ex) { Debug.WriteLine(ex); } } Employee selectedItem; public Employee SelectedItem { get { return selectedItem; } set { SetProperty(ref selectedItem, value); if(value != null) ExecuteSelectItem(); } } async Task ExecuteSelectItem() { if(SelectedItem == null) return; var tempGuid = SelectedItem.Oid; SelectedItem = null; await Shell.Current.GoToAsync($"{nameof(ItemDetailPage)}?itemGuid={tempGuid.Tostring()}"); }
In the
ItemsPage.xaml
file, add the ListView component with a custom DataTemplate instead of refreshing collection.<ListView x:Name="ItemsListView" ItemsSource="{Binding Items}" VerticalOptions="FillAndExpand" HasUnevenRows="true" RefreshCommand="{Binding LoadDataCommand}" IsPullToRefreshEnabled="true" IsRefreshing="{Binding IsBusy, Mode=OneWay}" CachingStrategy="RecycleElement" SelectedItem="{Binding SelectedItem, Mode=TwoWay}"> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <StackLayout Orientation="Horizontal"> <StackLayout Padding="10" HorizontalOptions="FillAndExpand"> <Label Text="{Binding FullName}" LineBreakMode="NoWrap" Style="{DynamicResource ListItemTextStyle}" FontSize="16" /> <Label Text="{Binding Department}" LineBreakMode="NoWrap" Style="{DynamicResource ListItemDetailTextStyle}" FontSize="13" /> </StackLayout> </StackLayout> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView>
-
Add the Add Item and Log Out buttons.
Note: In the command's
canExecute
function, you can use the IsGrantedExtensions Methods to disable a command if the current user is not authorized to do the corresponding opeation.public ItemsViewModel() { //... AddItemCommand = new Command(async () => { await ExecuteAddItemCommand(); }, ()=> XpoHelper.Security.CanCreate<Employee>()); LogOutCommand = new Command(() => ExecuteLogOutCommand()); } public Command AddItemCommand { get; set; } async Task ExecuteAddItemCommand() { await Shell.Current.GoToAsync($"{nameof(ItemDetailPage)}?itemGuid="); } public Command LogOutCommand { get; set; } async Task ExecuteLogOutCommand() { XpoHelper.Logoff(); await Shell.Current.GoToAsync($"//{nameof(LoginPage)}"); }
In the ItemsPage.xaml file, add the following ToolBar items.
<ContentPage.ToolbarItems> <ToolbarItem Text="Add" Command="{Binding AddItemCommand}" /> <ToolbarItem Text="LogOut" Command="{Binding LogOutCommand}" /> </ContentPage.ToolbarItems>
-
Implement the filter bar.
A user may want to see a list of employees from a specific depertment. To implement this, put a Picker control and bind it to the
Department
andSelectedDepartments
ViewModel properties.ObservableCollection<Department> departments; public ObservableCollection<Department> Departments { get { return departments; } set { SetProperty(ref departments, value); } } Department selectedDepartment; public Department SelectedDepartment { get { return selectedDepartment; } set { SetProperty(ref selectedDepartment, value); FilterByDepartment(); } } void FilterByDepartment() { if(SelectedDepartment != null) { LoadEmployees(); var items = Items.Where(w => w.Department == SelectedDepartment); Items = new ObservableCollection<Employee>(items); } else { LoadEmployees(); } } public ItemsViewModel() { //.. Departments = new ObservableCollection<Department>(); //.. ExecuteLoadDepartmentsCommand(); LoadDataCommand = new Command(() => { //.. ExecuteLoadDepartmentsCommand(); }); //.. } public void LoadDepartments() { try { var items = UnitOfWork.Query<Department>().ToList(); Departments = new ObservableCollection<Department>(items); } catch(Exception ex) { Debug.WriteLine(ex); } } void ExecuteLoadDepartmentsCommand() { if(IsBusy) return; IsBusy = true; LoadDepartments(); IsBusy = false; }
<StackLayout> <Picker Title="Filter" ItemsSource="{Binding Departments}" SelectedItem="{Binding SelectedDepartment}"/> <ListView /> </StackLayout>
Change the ViewModels\ItemDetailViewModel.cs and ViewModels\ItemDetailPage.xaml files as shown below.
-
In the
ItemDetailViewModel
class add[QueryProperty(nameof(ItemGuid), "itemGuid")] public class ItemDetailViewModel : XpoViewModel { Employee item; public Employee Item { get { return item; } set { SetProperty(ref item, value); } } bool isNewItem; public bool IsNewItem { get { return isNewItem; } set { SetProperty(ref isNewItem, value); } } Guid? oid; public Guid? Oid { get { return Oid; } set { SetProperty(ref Oid, value); Load();} } public Load() { //.. IsNewItem = (Oid == null); if(isNewItem) { Item = new Employee(UnitOfWork) { FirstName = "First name", LastName = "Last Name" }; Title = "New employee"; } else { Item = UnitOfWork.GetObjectByKey<Employee>(Oid); Title = Item?.FullName; } //.. } }
In the
ItemDetailPage.xaml
add aGrid
with following parameters<Grid ColumnSpacing="20" Padding="15"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Label Text="FirstName" FontSize="Medium" Grid.Row="0" Grid.Column="0" /> <Entry Text="{Binding Item.FirstName}" IsReadOnly="{Binding ReadOnly}" FontSize="Small" Margin="0" Grid.Row="1" Grid.Column="0" /> <Label Text="LastName" FontSize="Medium" Grid.Row="2" Grid.Column="0" /> <Entry Text="{Binding Item.LastName}" IsReadOnly="{Binding ReadOnly}" FontSize="Small" Margin="0" Grid.Row="3" Grid.Column="0" /> <Label Text="Department" IsVisible="{Binding CanReadDepartment}" FontSize="Medium" Grid.Row="4" Grid.Column="0" /> <Picker IsVisible="{Binding CanReadDepartment}" IsEnabled="{Binding CanReadDepartment}" ItemsSource="{Binding Departments}" ItemDisplayBinding="{Binding Title}" SelectedItem="{Binding Item.Department}" FontSize="Small" Margin="0" Grid.Row="5" Grid.Column="0"/> </Grid>
-
Picker
If a user is allowed to modify the
Department
property, aPicker
with selectable options is shown. In theItemDetailVeiwModel
class add following code:List<Department> departments; public List<Department> Departments { get { return departments; } set { SetProperty(ref departments, value); } } bool canReadDepartment; public bool CanReadDepartment { get { return canReadDepartment; } set { SetProperty(ref canReadDepartment, value); } } public Load() { Departments = UnitOfWork.Query<Department>().ToList(); //Has to be the first line; //.. CanReadDepartment = XpoHelper.Security.CanRead(Item, "Department"); CanWriteDepartment = XpoHelper.Security.CanWrite(Item, "Department"); if (isNewItem && CanWriteDepartment) { Item.Department = Departments?[0]; } }
-
Buttons
Save
andDelete
buttons availability depends on security. We will bind them to commands, so we can control whether a button is active.In the
ItemDetailViewModel
class, add commands and security properties:bool readOnly; public bool ReadOnly { get { return readOnly; } set { SetProperty(ref readOnly, value); CommandUpdate.ChangeCanExecute(); } } bool canDelete; public bool CanDelete { get { return canDelete; } set { SetProperty(ref canDelete, value); CommandDelete.ChangeCanExecute(); } } public Command CommandDelete { get; private set; } public Command CommandUpdate { get; private set; } public ItemDetailViewModel() { CommandDelete = new Command(async () => { await DeleteItemAndGoBack(); }, () => CanDelete && !isNewItem); CommandUpdate = new Command(async () => { await SaveItemAndGoBack(); }, () => !ReadOnly); } public Load(){ //.. CanDelete = XpoHelper.Security.CanDelete(Item); ReadOnly = !XpoHelper.Security.CanWrite(Item); //.. } async Task DeleteItemAndGoBack() { UnitOfWork.Delete(Item); await UnitOfWork.CommitChangesAsync(); await Shell.Current.Navigation.PopAsync(); } async Task SaveItemAndGoBack() { UnitOfWork.Save(Item); await UnitOfWork.CommitChangesAsync(); await Shell.Current.Navigation.PopAsync(); }
In the
ItemDetailPage.xaml
, add Toolbar items with the following parameters:<ContentPage.ToolbarItems> <ToolbarItem Text="Delete" Command="{Binding CommandDelete}" /> <ToolbarItem Text="Save" Command="{Binding CommandUpdate}" /> </ContentPage.ToolbarItems>
Finally add a constructor to the
ItemsDetailPage.xaml.cs
class to bind ViewModel and Page together.public ItemDetailPage() { InitializeComponent(); BindingContext = new ItemDetailViewModel(); }
To seed the data in the database, add the UpdateDataBase
method and call this method from the XpoHelper
static constructor:
static XpoHelper() {
//..
UpdateDataBase();
//..
}
static void UpdateDataBase() {
var space = GetObjectSpaceProvider().CreateUpdatingObjectSpace(true);
XafSolution.Module.DatabaseUpdate.Updater updater = new XafSolution.Module.DatabaseUpdate.Updater(space, null);
updater.UpdateDatabaseAfterUpdateSchema();
}
- Log in as 'User' with an empty password.
- Notice that secured data is not displayed.
- Press the Logout button and log in as 'Admin' to see all records.