Net Core - Xamarin Forms - Xamarin Classic - MVVM Cross
Net Core - Xamarin Forms - Xamarin Classic - MVVM Cross
Net Core - Xamarin Forms - Xamarin Classic - MVVM Cross
By Zulu
Medellín 2019
Index
Modify DB 10
Add API 40
Adding Images 43
Consuming RestFull 60
Modifying users 83
1
Add Font Awesome for Icons 94
Add Roles 95
Redirect Pages 98
Not Authorized 98
Handle Not Found Errors Gracefully 99
Manage Not Found Pages 100
3
Making the Shop Project With MVVM Cross 433
Core First Part (Login) 433
Android First Part (Login) 439
iOS First Part (Login) 445
Core Second Part (Products List) 450
Android Second Part (Products List) 455
Core Third Part (Add Product) 457
Android Third Part (Add Product) 462
Core Fourth Part (Register User) 467
Android Fourth Part (Register User) 473
Core Fifth Part (Product Details) 486
Android Fifth Part (Product Details) 492
Core Sixth Part (Check internet connection) 499
Android Sixth Part (Check internet connection) 501
Android Seventh Part (Toolbar) 502
Core Eighth Part (Confirm Delete) 525
Android Eighth Part (Confirm Delete) 527
Core Ninth Part (Change User, Change Password and Drawer Menu) 528
Android Ninth Part (Change User, Change Password and Drawer Menu) 543
Core Tenth Part (Taking images from camera or gallery) 577
Android Tenth Part (Taking images from camera or gallery) 582
Android Eleventh Part (Maps and Geolocation) 601
Core Twelfth Part (Custom Dialogs) 617
Android Twelfth Part (Custom Dialogs) 619
Backend
Front End API
Web
Common (.NET Standard)
In Visual Studio, you must build something similar to:
5
Create the Database
Note: in this project we’ll work with entity framework code first, if you want to work with EF database first, I recommend this article:
https://docs.microsoft.com/en-us/ef/core/get-started/aspnetcore/existing-db
using System;
using System.ComponentModel.DataAnnotations;
6
public decimal Price { get; set; }
[Display(Name = "Image")]
public string ImageUrl { get; set; }
using Common.Models;
using Microsoft.EntityFrameworkCore;
7
3. Add the connection string to the configuration json file (see the SQL Server Object Explorer):
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection":
"Server=(localdb)\\ProjectsV13;Database=Shop;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
Note: You must be sure of the servers names in your installation, you can check it out, by clicking in SQL Server Object
Explorer:
In this case, there are three available servers: (localdb)\MSSQLLocalDB, (localdb)\ProjectsV13 and (localdb)\v11.0. Or
you can explore your server by clicking on “Add SQL Server” icon:
8
4. Add the database injection in startup class (before MVC services lines):
services.AddDbContext<DataContext>(cfg =>
{
cfg.UseSqlServer(this.Configuration.GetConnectionString("DefaultConnection"));
});
5. Run this commands by command line in the same folder that is the web project:
9
dotnet ef database update
dotnet ef migrations add InitialDb
dotnet ef database update
PM> update-database
PM> add-migration InitialDb
PM> update-database
Modify DB
1. Modify the model product by:
using System;
using System.ComponentModel.DataAnnotations;
10
public int Id { get; set; }
[MaxLength(50, ErrorMessage = "The field {0} only can contain a maximum {1} characters")]
[Required]
public string Name { get; set; }
[Display(Name = "Image")]
public string ImageUrl { get; set; }
11
PM> add-migration ModifyProducts
PM> update-database
3. Test it.
using System;
using System.Linq;
using System.Threading.Tasks;
using Common.Models;
if (!this.context.Products.Any())
{
12
this.AddProduct("First Product");
this.AddProduct("Second Product");
this.AddProduct("Third Product");
await this.context.SaveChangesAsync();
}
}
using Data;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
13
host.Run();
}
3. Add the injection for the seeder in Startup class (before cookie policy options lines):
services.AddTransient<SeedDb>();
4. Test it.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
14
using Common.Models;
15
{
this.context.Products.Remove(product);
}
using System.Collections.Generic;
using System.Threading.Tasks;
using Common.Models;
IEnumerable<Product> GetProducts();
16
Task<bool> SaveAllAsync();
3. Replace the controller to uses the repository and not uses the database context:
using Data;
using Data.Entities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks;
17
return NotFound();
}
return View(product);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Product product)
{
if (ModelState.IsValid)
{
this.repository.AddProduct(product);
await this.repository.SaveAllAsync();
return RedirectToAction(nameof(Index));
}
return View(product);
}
18
if (id == null)
{
return NotFound();
}
return View(product);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Product product)
{
if (ModelState.IsValid)
{
try
{
this.repository.UpdateProduct(product);
await this.repository.SaveAllAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!this.repository.ProductExists(product.Id))
{
return NotFound();
}
else
19
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(product);
}
return View(product);
}
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var product = this.repository.GetProduct(id);
this.repository.RemoveProduct(product);
await this.repository.SaveAllAsync();
20
return RedirectToAction(nameof(Index));
}
}
4. Add the injection for the repository in Startup class (before cookie policy options lines):
services.AddScoped<IRepository, Repository>();
5. Test it.
using Microsoft.AspNetCore.Identity;
using Entities;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
21
public DbSet<Product> Products { get; set; }
4. Drop the database and add the new migrations with those commands:
PM> drop-database
PM> add-migration Users
PM> update-database
using System;
using System.Linq;
using System.Threading.Tasks;
using Common.Models;
using Microsoft.AspNetCore.Identity;
22
{
private readonly DataContext context;
private readonly UserManager<User> userManager;
private Random random;
23
}
if (!this.context.Products.Any())
{
this.AddProduct("First Product", user);
this.AddProduct("Second Product", user);
this.AddProduct("Third Product", user);
await this.context.SaveChangesAsync();
}
}
24
cfg.Password.RequiredUniqueChars = 0;
cfg.Password.RequireLowercase = false;
cfg.Password.RequireNonAlphanumeric = false;
cfg.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<DataContext>();
services.AddDbContext<DataContext>(cfg =>
{
cfg.UseSqlServer(this.Configuration.GetConnectionString("DefaultConnection"));
});
services.AddTransient<SeedDb>();
services.AddScoped<IRepository, Repository>();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
25
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
7. Test it.
1. Create the folder Helpers and inside it add the interface IUserHelper:
using System.Threading.Tasks;
using Data.Entities;
using Microsoft.AspNetCore.Identity;
26
public interface IUserHelper
{
Task<User> GetUserByEmailAsync(string email);
using System.Threading.Tasks;
using Data.Entities;
using Microsoft.AspNetCore.Identity;
27
}
using System.Linq;
using System.Threading.Tasks;
28
6. In the same folder add the implementation (GenericRepository):
using System.Linq;
using System.Threading.Tasks;
using Entities;
using Microsoft.EntityFrameworkCore;
29
await SaveAllAsync();
}
using Entities;
30
{
}
using Entities;
services.AddScoped<IProductRepository, ProductRepository>();
using System.Threading.Tasks;
using Data;
using Data.Entities;
using Helpers;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
31
private readonly IUserHelper userHelper;
// GET: Products
public IActionResult Index()
{
return View(this.productRepository.GetAll());
}
// GET: Products/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
return View(product);
}
32
// GET: Products/Create
public IActionResult Create()
{
return View();
}
// POST: Products/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Product product)
{
if (ModelState.IsValid)
{
// TODO: Pending to change to: this.User.Identity.Name
product.User = await this.userHelper.GetUserByEmailAsync("jzuluaga55@gmail.com");
await this.productRepository.CreateAsync(product);
return RedirectToAction(nameof(Index));
}
return View(product);
}
// GET: Products/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
33
{
return NotFound();
}
return View(product);
}
// POST: Products/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Product product)
{
if (ModelState.IsValid)
{
try
{
// TODO: Pending to change to: this.User.Identity.Name
product.User = await this.userHelper.GetUserByEmailAsync("jzuluaga55@gmail.com");
await this.productRepository.UpdateAsync(product);
}
catch (DbUpdateConcurrencyException)
{
if (!await this.productRepository.ExistAsync(product.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
34
}
return View(product);
}
// GET: Products/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
return View(product);
}
// POST: Products/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var product = await this.productRepository.GetByIdAsync(id);
await this.productRepository.DeleteAsync(product);
return RedirectToAction(nameof(Index));
}
}
35
12. Modify the SeedDb:
using System;
using System.Linq;
using System.Threading.Tasks;
using Entities;
using Microsoft.AspNetCore.Identity;
using Shop.Web.Helpers;
// Add user
var user = await this.userHelper.GetUserByEmail("jzuluaga55@gmail.com");
if (user == null)
{
user = new User
36
{
FirstName = "Juan",
LastName = "Zuluaga",
Email = "jzuluaga55@gmail.com",
UserName = "jzuluaga55@gmail.com",
PhoneNumber = "3506342747"
};
// Add products
if (!this.context.Products.Any())
{
this.AddProduct("iPhone X", user);
this.AddProduct("Magic Mouse", user);
this.AddProduct("iWatch Series 4", user);
await this.context.SaveChangesAsync();
}
}
37
Stock = this.random.Next(100),
User = user
});
}
}
14. Now to take advance the this implementation, we’ll create another entity that we’ll use nearly. Add the entity Country:
using System.ComponentModel.DataAnnotations;
[MaxLength(50, ErrorMessage = "The field {0} only can contain {1} characters length.")]
[Required]
[Display(Name = "Country")]
public string Name { get; set; }
}
using Entities;
38
using Entities;
services.AddScoped<ICountryRepository, CountryRepository>();
19. Save all and run those commands to update the database:
39
Add API
1. Create the API controller, this is an example (in Web.Controllers.API):
using Data;
using Microsoft.AspNetCore.Mvc;
[Route("api/[Controller]")]
public class ProductsController : Controller
{
private readonly IProductRepository productRepository;
[HttpGet]
public IActionResult GetProducts()
{
return this.Ok(this.productRepository.GetAll());
}
}
2. Test it.
40
41
42
Adding Images
1. In Web the folder Models and the class MainViewModel.
using System.ComponentModel.DataAnnotations;
using Data.Entities;
using Microsoft.AspNetCore.Http;
43
public class ProductViewModel : Product
{
[Display(Name = "Image")]
public IFormFile ImageFile { get; set; }
}
@model Shop.Web.Models.ProductViewModel
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<h4>Product</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create" enctype="multipart/form-data">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
44
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ImageFile" class="control-label"></label>
<input asp-for="ImageFile" class="form-control" type="file" />
<span asp-validation-for="ImageFile" class="text-danger"></span>
</div>
<div class="form-group">
…
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(ProductViewModel view)
{
if (ModelState.IsValid)
{
var path = string.Empty;
45
}
path = $"~/images/Products/{view.ImageFile.FileName}";
}
return View(view);
}
46
<td>
@if (!string.IsNullOrEmpty(item.ImageUrl))
{
<img src="@Url.Content(item.ImageUrl)" alt="Image" style="width:100px;height:150px;max-width: 100%; height: auto;" />
}
</td>
// GET: Products/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
47
return new ProductViewModel
{
Id = product.Id,
ImageUrl = product.ImageUrl,
IsAvailabe = product.IsAvailabe,
LastPurchase = product.LastPurchase,
LastSale = product.LastSale,
Name = product.Name,
Price = product.Price,
Stock = product.Stock,
User = product.User
};
}
// POST: Products/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(ProductViewModel view)
{
if (ModelState.IsValid)
{
try
{
var path = view.ImageUrl;
48
}
path = $"~/images/Products/{view.ImageFile.FileName}";
}
@model Shop.Web.Models.ProductViewModel
@{
ViewData["Title"] = "Edit";
49
}
<h2>Edit</h2>
<h4>Product</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit" enctype="multipart/form-data">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="ImageUrl" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ImageFile" class="control-label"></label>
<input asp-for="ImageFile" class="form-control" type="file" />
<span asp-validation-for="ImageFile" class="text-danger"></span>
</div>
<div class="form-group">
50
<label asp-for="LastPurchase" class="control-label"></label>
<input asp-for="LastPurchase" class="form-control" />
<span asp-validation-for="LastPurchase" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="LastSale" class="control-label"></label>
<input asp-for="LastSale" class="form-control" />
<span asp-validation-for="LastSale" class="text-danger"></span>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input asp-for="IsAvailabe" /> @Html.DisplayNameFor(model => model.IsAvailabe)
</label>
</div>
</div>
<div class="form-group">
<label asp-for="Stock" class="control-label"></label>
<input asp-for="Stock" class="form-control" />
<span asp-validation-for="Stock" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-success">Back to List</a>
</div>
</form>
</div>
<div class="col-md-4">
51
@if (!string.IsNullOrEmpty(Model.ImageUrl))
{
<img src="@Url.Content(Model.ImageUrl)" alt="Image" style="width:200px;height:300px;max-width: 100%; height: auto;" />
}
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
9. Test it.
<dd>
@if (!string.IsNullOrEmpty(Model.ImageUrl))
{
<img src="@Url.Content(Model.ImageUrl)" alt="Image" style="width:200px;height:300px;max-width: 100%; height: auto;" />
}
</dd>
<dd>
@if (!string.IsNullOrEmpty(Model.ImageUrl))
{
<img src="@Url.Content(Model.ImageUrl)" alt="Image" style="width:200px;height:300px;max-width: 100%; height: auto;" />
}
</dd>
52
.
13. Finally add this property to Product entity:
return $"https://shopzulu.azurewebsites.net{this.ImageUrl.Substring(1)}";
}
}
14. Ant test the API and publish the Changes in Azure.
using Entities;
using System.Linq;
53
using System.Linq;
using Entities;
using Microsoft.EntityFrameworkCore;
4. Test it.
54
Starting with Xamarin Forms
1. Create the folder ViewModels and inside it add the class ProductViewModel.
2. Create the folder Infrastructure and inside it add the class InstanceLocator.
public InstanceLocator()
{
this.Main = new MainViewModel();
}
}
4. Add the folder Views and inside it, create the LoginPage:
56
<Button
Command="{Binding LoginCommand}"
Text="Login">
</Button>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
5. Add the NuGet MvvmLigthLibsStd10. (Plaase search as: Mvvm Ligth Libs Std)
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;
using Xamarin.Forms;
57
if (string.IsNullOrEmpty(this.Password))
{
await Application.Current.MainPage.DisplayAlert("Error", "You must enter a password", "Accept");
return;
}
if (!this.Email.Equals("jzuluaga55@gmail.com") || !this.Password.Equals("123456"))
{
await Application.Current.MainPage.DisplayAlert("Error", "Incorrect user or password", "Accept");
return;
}
public MainViewModel()
{
this.Login = new LoginViewModel();
}
}
using Views;
58
using Xamarin.Forms;
9. Test it.
59
Fix Bug to Don’t Replace Images
1. Modify the MVC ProductsController in Create and Edit:
path = Path.Combine(
Directory.GetCurrentDirectory(),
"wwwroot\\images\\Products",
file);
path = $"~/images/Products/{file}";
}
2. Test it.
Consuming RestFull
1. Add the NuGet Newtonsoft.Json to project Commond.
2. Add the folder Models and inside it those classes (I recommend use the http://json2csharp.com/ page):
using System;
60
using Newtonsoft.Json;
[JsonProperty("lastName")]
public string LastName { get; set; }
[JsonProperty("id")]
public Guid Id { get; set; }
[JsonProperty("userName")]
public string UserName { get; set; }
[JsonProperty("normalizedUserName")]
public string NormalizedUserName { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("normalizedEmail")]
public string NormalizedEmail { get; set; }
[JsonProperty("emailConfirmed")]
public bool EmailConfirmed { get; set; }
[JsonProperty("passwordHash")]
public string PasswordHash { get; set; }
[JsonProperty("securityStamp")]
61
public string SecurityStamp { get; set; }
[JsonProperty("concurrencyStamp")]
public Guid ConcurrencyStamp { get; set; }
[JsonProperty("phoneNumber")]
public string PhoneNumber { get; set; }
[JsonProperty("phoneNumberConfirmed")]
public bool PhoneNumberConfirmed { get; set; }
[JsonProperty("twoFactorEnabled")]
public bool TwoFactorEnabled { get; set; }
[JsonProperty("lockoutEnd")]
public object LockoutEnd { get; set; }
[JsonProperty("lockoutEnabled")]
public bool LockoutEnabled { get; set; }
[JsonProperty("accessFailedCount")]
public long AccessFailedCount { get; set; }
}
And:
using Newtonsoft.Json;
using System;
62
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("price")]
public decimal Price { get; set; }
[JsonProperty("imageUrl")]
public string ImageUrl { get; set; }
[JsonProperty("lastPurchase")]
public DateTime LastPurchase { get; set; }
[JsonProperty("lastSale")]
public DateTime LastSale { get; set; }
[JsonProperty("isAvailabe")]
public bool IsAvailabe { get; set; }
[JsonProperty("stock")]
public double Stock { get; set; }
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("imageFullPath")]
public Uri ImageFullPath { get; set; }
}
63
public class Response
{
public bool IsSuccess { get; set; }
4. In Common project add the folder Services and inside it add the class ApiService.
using System;
using System.Collections.Generic;
using System.Net.Http;
using Models;
using Newtonsoft.Json;
using System.Threading.Tasks;
64
if (!response.IsSuccessStatusCode)
{
return new Response
{
IsSuccess = false,
Message = result,
};
}
65
x:Class="Shop.UIForms.Views.ProductsPage"
BindingContext="{Binding Main, Source={StaticResource Locator}}"
Title="Products">
<ContentPage.Content>
<StackLayout
BindingContext="{Binding Products}"
Padding="5">
<ListView
HasUnevenRows="True"
ItemsSource="{Binding Products}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Source="{Binding ImageFullPath}"
WidthRequest="100">
</Image>
<StackLayout
Grid.Column="1"
VerticalOptions="Center">
<Label
FontAttributes="Bold"
FontSize="Medium"
Text="{Binding Name}"
TextColor="Black">
</Label>
66
<Label
Text="{Binding Price, StringFormat='{0:C2}'}"
TextColor="Black">
</Label>
</StackLayout>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
67
{
return;
}
backingField = value;
OnPropertyChanged(propertyName);
}
}
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Common.Models;
using Common.Services;
using Xamarin.Forms;
public ProductsViewModel()
{
this.apiService = new ApiService();
this.LoadProducts();
68
}
69
public MainViewModel()
{
instance = this;
this.Login = new LoginViewModel();
}
return instance;
}
}
if (!this.Email.Equals("jzuluaga55@gmail.com") || !this.Password.Equals("123456"))
{
await Application.Current.MainPage.DisplayAlert("Error", "Incorrect user or password", "Accept");
return;
}
10. Now add an activity indicator and refresh to the list view. Modify the ProductsPage:
<ListView
IsPullToRefreshEnabled="True"
70
IsRefreshing="{Binding IsRefreshing}"
HasUnevenRows="True"
ItemsSource="{Binding Products}"
RefreshCommand="{Binding RefreshCommand}">
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Input;
using Common.Models;
using Common.Services;
using GalaSoft.MvvmLight.Command;
using Xamarin.Forms;
71
public ICommand RefreshCommand => new RelayCommand(this.LoadProducts);
public ProductsViewModel()
{
this.apiService = new ApiService();
this.LoadProducts();
}
72
12. Test it.
base.OnModelCreating(modelBuilder);
}
using System.ComponentModel.DataAnnotations;
73
public class LoginViewModel
{
[Required]
[EmailAddress]
public string Username { get; set; }
[Required]
public string Password { get; set; }
Task LogoutAsync();
Implementation:
74
model.Username,
model.Password,
model.RememberMe,
false);
}
using System.Linq;
using System.Threading.Tasks;
using Helpers;
using Microsoft.AspNetCore.Mvc;
using Models;
75
return this.RedirectToAction("Index", "Home");
}
return this.View();
}
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (this.ModelState.IsValid)
{
var result = await this.userHelper.LoginAsync(model);
if (result.Succeeded)
{
if (this.Request.Query.Keys.Contains("ReturnUrl"))
{
return this.Redirect(this.Request.Query["ReturnUrl"].First());
}
76
}
@model Shop.Web.Models.LoginViewModel
@{
ViewData["Title"] = "Login";
}
<h2>Login</h2>
<div class="row">
<div class="col-md-4 offset-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="Username">Username</label>
<input asp-for="Username" class="form-control" />
<span asp-validation-for="Username" class="text-warning"></span>
</div>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<div class="form-group">
<label asp-for="Password">Password</label>
<input asp-for="Password" type="password" class="form-control" />
<span asp-validation-for="Password" class="text-warning"></span>
</div>
<div class="form-group">
<div class="form-check">
<input asp-for="RememberMe" type="checkbox" class="form-check-input" />
<label asp-for="RememberMe" class="form-check-label">Remember Me?</label>
</div>
<span asp-validation-for="RememberMe" class="text-warning"></span>
77
</div>
<div class="form-group">
<input type="submit" value="Login" class="btn btn-success" />
<a asp-action="Register" class="btn btn-primary">Register New User</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
[Authorize]
7. If the any user is logged in, don’t show the products option in menu:
78
@if (this.User.Identity.IsAuthenticated)
{
<li><a asp-area="" asp-controller="Products" asp-action="Index">Products</a></li>
}
8. Test it.
using System.ComponentModel.DataAnnotations;
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
public string Username { get; set; }
[Required]
public string Password { get; set; }
79
[Required]
[Compare("Password")]
public string Confirm { get; set; }
}
[HttpPost]
public async Task<IActionResult> Register(RegisterNewUserViewModel model)
{
if (this.ModelState.IsValid)
{
var user = await this.userHelper.GetUserByEmailAsync(model.Username);
if (user == null)
{
user = new User
{
FirstName = model.FirstName,
LastName = model.LastName,
Email = model.Username,
UserName = model.Username
};
80
return this.View(model);
}
if (result2.Succeeded)
{
return this.RedirectToAction("Index", "Home");
}
return this.View(model);
}
@model Shop.Web.Models.RegisterNewUserViewModel
@{
81
ViewData["Title"] = "Register";
}
<div class="row">
<div class="col-md-4 offset-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="FirstName">First Name</label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="LastName">Last Name</label>
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="Username">Username</label>
<input asp-for="Username" class="form-control" />
<span asp-validation-for="Username" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="Password">Password</label>
<input asp-for="Password" type="password" class="form-control" />
<span asp-validation-for="Password" class="text-warning"></span>
82
</div>
<div class="form-group">
<label asp-for="Confirm">Confirm</label>
<input asp-for="Confirm" type="password" class="form-control" />
<span asp-validation-for="Confirm" class="text-warning"></span>
</div>
<div class="form-group">
<input type="submit" value="Register New User" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
4. Test it.
Modifying users
1. Create those new models (in Web.Models):
using System.ComponentModel.DataAnnotations;
83
public string FirstName { get; set; }
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
}
And:
using System.ComponentModel.DataAnnotations;
[Required]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[Required]
[Compare("NewPassword")]
public string Confirm { get; set; }
}
84
And the implementation:
return this.View(model);
}
[HttpPost]
public async Task<IActionResult> ChangeUser(ChangeUserViewModel model)
{
if (this.ModelState.IsValid)
{
85
var user = await this.userHelper.GetUserByEmailAsync(this.User.Identity.Name);
if (user != null)
{
user.FirstName = model.FirstName;
user.LastName = model.LastName;
var respose = await this.userHelper.UpdateUserAsync(user);
if (respose.Succeeded)
{
this.ViewBag.UserMessage = "User updated!";
}
else
{
this.ModelState.AddModelError(string.Empty, respose.Errors.FirstOrDefault().Description);
}
}
else
{
this.ModelState.AddModelError(string.Empty, "User no found.");
}
}
return this.View(model);
}
@model Shop.Web.Models.ChangeUserViewModel
@{
ViewData["Title"] = "Register";
}
<h2>Update User</h2>
86
<div class="row">
<div class="col-md-4 offset-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="FirstName">First Name</label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="LastName">Last Name</label>
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-warning"></span>
</div>
<div class="form-group">
<input type="submit" value="Update" class="btn btn-primary" />
<a asp-action="ChangePassword" class="btn btn-success">Change Password</a>
</div>
<div class="text-success">@ViewBag.UserMessage</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
87
5. And now this actions in the controller to password modification:
[HttpPost]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
{
if (this.ModelState.IsValid)
{
var user = await this.userHelper.GetUserByEmailAsync(this.User.Identity.Name);
if (user != null)
{
var result = await this.userHelper.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
if (result.Succeeded)
{
return this.RedirectToAction("ChangeUser");
}
else
{
this.ModelState.AddModelError(string.Empty, result.Errors.FirstOrDefault().Description);
}
}
else
{
this.ModelState.AddModelError(string.Empty, "User no found.");
}
}
88
return this.View(model);
}
@model Shop.Web.Models.ChangePasswordViewModel
@{
ViewData["Title"] = "Register";
}
@section Scripts {
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
}
<h2>Change Password</h2>
<div class="row">
<div class="col-md-4 offset-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="OldPassword">Current password</label>
<input asp-for="OldPassword" type="password" class="form-control" />
<span asp-validation-for="OldPassword" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="NewPassword">New password</label>
<input asp-for="NewPassword" type="password" class="form-control" />
<span asp-validation-for="NewPassword" class="text-warning"></span>
</div>
89
<div class="form-group">
<label asp-for="Confirm">Confirm</label>
<input asp-for="Confirm" type="password" class="form-control" />
<span asp-validation-for="Confirm" class="text-warning"></span>
</div>
<div class="form-group">
<input type="submit" value="Change password" class="btn btn-primary" />
<a asp-action="ChangeUser" class="btn btn-success">Back to user</a>
</div>
</form>
</div>
</div>
7. Test it.
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\ProjectsV13;Database=Core3;Trusted_Connection=True;MultipleActiveResultSets=true"
},
90
"Tokens": {
"Key": "asdfghjikbnvcgfdsrtfyhgcvgfxdgc",
"Issuer": "localhost",
"Audience": "users"
}
}
public AccountController(
SignInManager<User> signInManager,
UserManager<User> userManager,
IConfiguration configuration)
{
this.signInManager = signInManager;
this.userManager = userManager;
this.configuration = configuration;
91
}
[HttpPost]
public async Task<IActionResult> CreateToken([FromBody] LoginViewModel model)
{
if (this.ModelState.IsValid)
{
var user = await this.userHelper.GetUserByEmailAsync(model.Username);
if (user != null)
{
var result = await this.userHelper.ValidatePasswordAsync(
user,
model.Password);
if (result.Succeeded)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
92
var results = new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
};
return this.BadRequest();
}
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
6. Add the new configuration for validate the tokens (before data context lines):
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(cfg =>
{
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = this.Configuration["Tokens:Issuer"],
ValidAudience = this.Configuration["Tokens:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Configuration["Tokens:Key"]))
};
});
93
7. Test it.
{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"devDependencies": {
"font-awesome": "^4.7.0"
}
}
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link href="~/node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<div class="form-group">
<button type="submit" class="btn btn-primary"><i class="fa fa-save"></i> Create</button>
<a asp-action="Index" class="btn btn-success"><i class="fa fa-chevron-left"></i> Back to List</a>
</div>
94
Add Roles
1. Add those methods to IUserHelper:
public UserHelper(
UserManager<User> userManager,
SignInManager<User> signInManager,
RoleManager<IdentityRole> roleManager)
{
this.userManager = userManager;
this.signInManager = signInManager;
this.roleManager = roleManager;
}
await this.userHelper.CheckRoleAsync("Admin");
await this.userHelper.CheckRoleAsync("Customer");
// Add user
var user = await this.userHelper.GetUserByEmailAsync("jzuluaga55@gmail.com");
if (user == null)
{
user = new User
{
FirstName = "Juan",
LastName = "Zuluaga",
Email = "jzuluaga55@gmail.com",
UserName = "jzuluaga55@gmail.com",
PhoneNumber = "3506342747"
96
};
// Add products
if (!this.context.Products.Any())
{
this.AddProduct("iPhone X", user);
this.AddProduct("Magic Mouse", user);
this.AddProduct("iWatch Series 4", user);
await this.context.SaveChangesAsync();
}
}
3. Now you can include the role in authorization annotation in methods Create, Edit and Delete in Products MVC controller:
[Authorize(Roles = "Admin")]
97
4. Test it.
Redirect Pages
(Thanks to Gonzalo Jaimes)
Not Authorized
1. Create NotAuthorized method on AccountController:
public IActionResult NotAuthorized()
{
return this.View();
}
@{
ViewData["Title"] = "NotAuthorized";
}
3. Modify Startup.cs to configure the Application Cookie Options (after cookies lines)
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Account/NotAuthorized";
options.AccessDeniedPath = "/Account/NotAuthorized";
});
4. Test it!
98
Handle Not Found Errors Gracefully
1. Create NotFoundViewResult Class (Inside Helpers Folder). This way we can customize the page depending on the
controller action.
using Microsoft.AspNetCore.Mvc;
using System.Net;
2. In the controller Action call the NotFoundViewResult method when you`ll expect a not found event
// GET: Products/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return new NotFoundViewResult("ProductNotFound");
}
return View(product);
99
}
3. Create the ProductNotFound action or any other custom view depending on what you want.
@{
ViewData["Title"] = "ProductNotFound";
}
5. Test it!.
100
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/error/{0}");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
[Route("error/404")]
public IActionResult Error404()
{
return View();
}
@{
ViewData["Title"] = "Error404";
}
101
<br />
<br />
<img src="~/images/gopher_head-min.png" />
<h2>Sorry, page not found</h2>
4. Test it!.
102
Orders Functionality
We need to build this:
* 1
Cities Countries
1
*
1 1 *
Users Orders
1 1
* *
Products OrderDetails
*
OrderDetailTm
ps
*
using System.ComponentModel.DataAnnotations;
[Required]
103
public User User { get; set; }
[Required]
public Product Product { get; set; }
[DisplayFormat(DataFormatString = "{0:C2}")]
public decimal Price { get; set; }
[DisplayFormat(DataFormatString = "{0:N2}")]
public double Quantity { get; set; }
[DisplayFormat(DataFormatString = "{0:C2}")]
public decimal Value { get { return this.Price * (decimal)this.Quantity; } }
}
using System.ComponentModel.DataAnnotations;
[Required]
public Product Product { get; set; }
[DisplayFormat(DataFormatString = "{0:C2}")]
public decimal Price { get; set; }
[DisplayFormat(DataFormatString = "{0:N2}")]
public double Quantity { get; set; }
104
[DisplayFormat(DataFormatString = "{0:C2}")]
public decimal Value { get { return this.Price * (decimal)this.Quantity; } }
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
[Required]
[Display(Name = "Order date")]
[DisplayFormat(DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", ApplyFormatInEditMode = false)]
public DateTime OrderDate { get; set; }
[Required]
public User User { get; set; }
[DisplayFormat(DataFormatString = "{0:N2}")]
public double Quantity { get { return this.Items == null ? 0 : this.Items.Sum(i => i.Quantity); } }
105
[DisplayFormat(DataFormatString = "{0:C2}")]
public decimal Value { get { return this.Items == null ? 0 : this.Items.Sum(i => i.Value); } }
}
4. Add the order and order detail temporarily to data context, it’s not necessary to add order detail, but I recommend to include
it.
using System.Linq;
using System.Threading.Tasks;
using Entities;
106
{
Task<IQueryable<Order>> GetOrdersAsync(string userName);
}
using System.Linq;
using System.Threading.Tasks;
using Entities;
using Helpers;
using Microsoft.EntityFrameworkCore;
107
{
return this.context.Orders
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.OrderByDescending(o => o.OrderDate);
}
return this.context.Orders
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Where(o => o.User == user)
.OrderByDescending(o => o.OrderDate);
}
}
services.AddScoped<ICountryRepository, CountryRepository>();
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<IUserHelper, UserHelper>();
using System.Threading.Tasks;
using Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[Authorize]
public class OrdersController : Controller
{
private readonly IOrderRepository orderRepository;
108
public OrdersController(IOrderRepository orderRepository)
{
this.orderRepository = orderRepository;
}
@model IEnumerable<Shop.Web.Data.Entities.Order>
@{
ViewData["Title"] = "Index";
}
@model IEnumerable<Shop.Web.Data.Entities.Order>
@{
ViewData["Title"] = "Index";
}
109
<h2>Orders</h2>
<p>
<a asp-action="Create" class="btn btn-primary">Create New</a>
</p>
<table class="table">
<thead>
<tr>
@if (this.User.IsInRole("Admin"))
{
<th>
@Html.DisplayNameFor(model => model.User.FullName)
</th>
}
<th>
@Html.DisplayNameFor(model => model.OrderDate)
</th>
<th>
@Html.DisplayNameFor(model => model.DeliveryDate)
</th>
<th>
# Lines
</th>
<th>
@Html.DisplayNameFor(model => model.Quantity)
</th>
<th>
@Html.DisplayNameFor(model => model.Value)
</th>
<th></th>
</tr>
</thead>
110
<tbody>
@foreach (var item in Model)
{
<tr>
@if (this.User.IsInRole("Admin"))
{
<th>
@Html.DisplayFor(modelItem => item.User.FullName)
</th>
}
<td>
@Html.DisplayFor(modelItem => item.OrderDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.DeliveryDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Items.Count())
</td>
<td>
@Html.DisplayFor(modelItem => item.Quantity)
</td>
<td>
@Html.DisplayFor(modelItem => item.Value)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-warning">Edit</a>
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-info">Details</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-danger">Delete</a>
</td>
</tr>
}
111
</tbody>
</table>
return this.context.OrderDetailTemps
.Include(o => o.Product)
.Where(o => o.User == user)
.OrderBy(o => o.Product.Name);
}
112
{
var model = await this.orderRepository.GetDetailTempsAsync(this.User.Identity.Name);
return this.View(model);
}
@model IEnumerable<Shop.Web.Data.Entities.OrderDetailTemp>
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<p>
<a asp-action="AddProduct" class="btn btn-success">Add Product</a>
<a asp-action="ConfirmOrder" class="btn btn-primary">Confirm Order</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Product.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Quantity)
</th>
<th>
113
@Html.DisplayNameFor(model => model.Value)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Product.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Quantity)
</td>
<td>
@Html.DisplayFor(modelItem => item.Value)
</td>
<td>
<a asp-action="Increase" asp-route-id="@item.Id" class="btn btn-warning"><i class="fa fa-plus"></i></a>
<a asp-action="Decrease" asp-route-id="@item.Id" class="btn btn-info"><i class="fa fa-minus"></i></a>
<a asp-action="DeleteItem" asp-route-id="@item.Id" class="btn btn-danger">Delete</a>
</td>
</tr>
}
</tbody>
</table>
114
18. Create the model to add products to order temporary:
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
IEnumerable<SelectListItem> GetComboProducts();
115
list.Insert(0, new SelectListItem
{
Text = "(Select a product...)",
Value = "0"
});
return list;
}
116
var orderDetailTemp = await this.context.OrderDetailTemps
.Where(odt => odt.User == user && odt.Product == product)
.FirstOrDefaultAsync();
if (orderDetailTemp == null)
{
orderDetailTemp = new OrderDetailTemp
{
Price = product.Price,
Product = product,
Quantity = model.Quantity,
User = user,
};
this.context.OrderDetailTemps.Add(orderDetailTemp);
}
else
{
orderDetailTemp.Quantity += model.Quantity;
this.context.OrderDetailTemps.Update(orderDetailTemp);
}
await this.context.SaveChangesAsync();
}
117
orderDetailTemp.Quantity += quantity;
if (orderDetailTemp.Quantity > 0)
{
this.context.OrderDetailTemps.Update(orderDetailTemp);
await this.context.SaveChangesAsync();
}
}
return View(model);
}
[HttpPost]
public async Task<IActionResult> AddProduct(AddItemViewModel model)
{
if (this.ModelState.IsValid)
{
await this.orderRepository.AddItemToOrderAsync(model, this.User.Identity.Name);
return this.RedirectToAction("Create");
}
return this.View(model);
118
}
@model Shop.Web.Models.AddItemViewModel
@{
ViewData["Title"] = "AddProduct";
}
<h2>Add Product</h2>
<h4>To Order</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="AddProduct">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ProductId" class="control-label"></label>
<select asp-for="ProductId" asp-items="Model.Products" class="form-control"></select>
<span asp-validation-for="ProductId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Quantity" class="control-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity" class="text-danger"></span>
</div>
<div class="form-group">
119
<input type="submit" value="Create" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-success">Back to List</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
And repository:
this.context.OrderDetailTemps.Remove(orderDetailTemp);
await this.context.SaveChangesAsync();
}
120
public async Task<IActionResult> DeleteItem(int? id)
{
if (id == null)
{
return NotFound();
}
await this.orderRepository.DeleteDetailTempAsync(id.Value);
return this.RedirectToAction("Create");
}
121
}
27. Add the confirm order method in the interface and implementation in IOrderRepository:
122
Quantity = o.Quantity
}).ToList();
this.context.Orders.Add(order);
this.context.OrderDetailTemps.RemoveRange(orderTmps);
await this.context.SaveChangesAsync();
return true;
}
[DisplayFormat(DataFormatString = "{0:N0}")]
public int Lines { get { return this.Items == null ? 0 : this.Items.Count(); } }
[DisplayFormat(DataFormatString = "{0:N2}")]
public double Quantity { get { return this.Items == null ? 0 : this.Items.Sum(i => i.Quantity); } }
@model IEnumerable<Core4.Data.Entities.Order>
@{
ViewData["Title"] = "Index";
123
}
<h2>Orders</h2>
<p>
<a asp-action="Create" class="btn btn-primary">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.OrderDate)
</th>
<th>
@Html.DisplayNameFor(model => model.DeliveryDate)
</th>
<th>
@Html.DisplayNameFor(model => model.Lines)
</th>
<th>
@Html.DisplayNameFor(model => model.Quantity)
</th>
<th>
@Html.DisplayNameFor(model => model.Value)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
124
<td>
@Html.DisplayFor(modelItem => item.OrderDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.DeliveryDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Lines)
</td>
<td>
@Html.DisplayFor(modelItem => item.Quantity)
</td>
<td>
@Html.DisplayFor(modelItem => item.Value)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-warning">Edit</a>
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-info">Details</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-danger">Delete</a>
</td>
</tr>
}
</tbody>
</table>
125
return this.RedirectToAction("Index");
}
return this.RedirectToAction("Create");
}
return this.OrderDate.ToLocalTime();
}
}
Change the index view to show this new property (and do the same for other data fields).
<th>
@Html.DisplayNameFor(model => model.OrderDateLocal)
</th>
And:
<td>
126
@Html.DisplayFor(modelItem => item.OrderDateLocal)
</td>
32. Fix the bug in OrderRepository in method GetOrdersAsync to get the user in the query.
@model IEnumerable<ShopPrep.Common.Models.OrderDetailTemp>
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<p>
<a asp-action="AddProduct" class="btn btn-success">Add Product</a>
<a asp-action="ConfirmOrder" class="btn btn-primary" id="btnConfirm">Confirm Order</a>
127
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Product.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th>
@Html.DisplayNameFor(model => model.Quantity)
</th>
<th>
@Html.DisplayNameFor(model => model.Value)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Product.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Quantity)
</td>
128
<td>
@Html.DisplayFor(modelItem => item.Value)
</td>
<td>
<a asp-action="Increase" asp-route-id="@item.Id" class="btn btn-warning"><i class="fa fa-plus"></i></a>
<a asp-action="Decrease" asp-route-id="@item.Id" class="btn btn-info"><i class="fa fa-minus"></i></a>
<a asp-action="DeleteItem" asp-route-id="@item.Id" class="btn btn-danger">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@section Scripts {
129
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$(document).ready(function () {
$("#btnConfirm").click(function () {
$("#confirmDialog").modal('show');
return false;
});
$("#btnNo").click(function () {
$("#confirmDialog").modal('hide');
return false;
});
$("#btnYes").click(function () {
window.location.href = '/Orders/ConfirmOrder';
});
});
</script>
}
2. Test it.
3. To add a validation to delete a product from the order, make this modifications in the view:
...
</td>
<td id="@item.Id">
<a asp-action="Increase" asp-route-id="@item.Id" class="btn btn-warning"><i class="fa fa-plus"></i></a>
<a asp-action="Decrease" asp-route-id="@item.Id" class="btn btn-info"><i class="fa fa-minus"></i></a>
<a asp-action="DeleteItem" asp-route-id="@item.Id" class="btn btn-danger" id="btnDeleteItem">Delete</a>
</td>
130
</tr>
}
</tbody>
</table>
131
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" id="btnYesDelete">Delete</button>
<button type="button" class="btn btn-success" id="btnNoDelete">No</button>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$(document).ready(function () {
var id = 0;
$("#btnConfirm").click(function () {
$("#confirmDialog").modal('show');
return false;
});
$("#btnNoConfirm").click(function () {
$("#confirmDialog").modal('hide');
return false;
});
$("#btnYesConfirm").click(function () {
window.location.href = '/Orders/ConfirmOrder';
});
$('a[id*=btnDeleteItem]').click(function () {
132
debugger;
id = $(this).parent()[0].id;
$("#deleteDialog").modal('show');
return false;
});
$("#btnNoDelete").click(function () {
$("#deleteDialog").modal('hide');
return false;
});
$("#btnYesDelete").click(function () {
window.location.href = '/Orders/DeleteItem/' + id;
});
});
</script>
}
4. Test it.
Date Picker
1. Add to de package json file this line:
{
"version": "1.0.0",
"name": "asp.net",
"private": true,
"devDependencies": {
"font-awesome": "^4.7.0",
"bootstrap-datepicker": "^1.8.0"
133
}
}
2. Save the file and copy the bootstrap date picker into folder root node modules.
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link href="~/node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<link rel="stylesheet" href="~/css/site.css" />
<link href="~/node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css" rel="stylesheet" />
</environment>
….
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
using System;
using System.ComponentModel.DataAnnotations;
134
public DateTime DeliveryDate { get; set; }
}
order.DeliveryDate = model.DeliveryDate;
this.context.Orders.Update(order);
await this.context.SaveChangesAsync();
}
135
{
if (id == null)
{
return NotFound();
}
return View(model);
}
[HttpPost]
public async Task<IActionResult> Deliver(DeliverViewModel model)
{
if (this.ModelState.IsValid)
{
await this.orderRepository.DeliverOrder(model);
return this.RedirectToAction("Index");
}
return this.View();
}
136
7. Add the view:
@model Shop.Web.Models.DeliverViewModel
@{
ViewData["Title"] = "Deliver";
}
<h2>Deliver</h2>
<h4>Order</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Deliver">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="DeliveryDate" class="control-label"></label>
<div class="input-group date" data-provide="datepicker">
<input asp-for="DeliveryDate" class="form-control" />
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
<span asp-validation-for="DeliveryDate" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
137
<a asp-action="Index" class="btn btn-success">Back to List</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
<td id="@item.Id">
<a asp-action="Deliver" asp-route-id="@item.Id" class="btn btn-info" id="btnDeliver">Deliver</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-danger" id="btnDelete">Delete</a>
</td>
9. Test it.
<div class="form-group">
<label asp-for="LastPurchase" class="control-label"></label>
<div class="input-group date" data-provide="datepicker">
<input asp-for="LastPurchase" class="form-control" />
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
<span asp-validation-for="LastPurchase" class="text-danger"></span>
</div>
138
<div class="form-group">
<label asp-for="LastSale" class="control-label"></label>
<div class="input-group date" data-provide="datepicker">
<input asp-for="LastSale" class="form-control" />
<span class="input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
<span asp-validation-for="LastSale" class="text-danger"></span>
</div>
using System.ComponentModel.DataAnnotations;
[Required]
[Display(Name = "City")]
[MaxLength(50, ErrorMessage = "The field {0} only can contain {1} characters length.")]
public string Name { get; set; }
}
139
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
[Required]
[Display(Name = "Country")]
[MaxLength(50, ErrorMessage = "The field {0} only can contain {1} characters length.")]
public string Name { get; set; }
[MaxLength(100, ErrorMessage = "The field {0} only can contain {1} characters length.")]
public string Address { get; set; }
140
4. Save all and run this commands to update the database, it’s important delete the database for ensure that all users have the
new fields:
PM> drop-database
PM> add-migration CountriesAndCities
PM> update-database
await this.CheckRole("Admin");
await this.CheckRole("Customer");
if (!this.context.Countries.Any())
{
var cities = new List<City>();
cities.Add(new City { Name = "Medellín" });
cities.Add(new City { Name = "Bogotá" });
cities.Add(new City { Name = "Calí" });
this.context.Countries.Add(new Country
{
Cities = cities,
Name = "Colombia"
});
await this.context.SaveChangesAsync();
141
}
using System.ComponentModel.DataAnnotations;
[Required]
[Display(Name = "City")]
[MaxLength(50, ErrorMessage = "The field {0} only can contain {1} characters length.")]
142
public string Name { get; set; }
}
using System.Linq;
using System.Threading.Tasks;
using Entities;
using Models;
using System.Linq;
using System.Threading.Tasks;
using Entities;
using Microsoft.EntityFrameworkCore;
using Models;
143
public class CountryRepository : GenericRepository<Country>, ICountryRepository
{
private readonly DataContext context;
this.context.Cities.Remove(city);
144
await this.context.SaveChangesAsync();
return country.Id;
}
this.context.Cities.Update(city);
await this.context.SaveChangesAsync();
return country.Id;
}
145
public async Task<City> GetCityAsync(int id)
{
return await this.context.Cities.FindAsync(id);
}
}
using System.Threading.Tasks;
using Data;
using Data.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Models;
[Authorize(Roles = "Admin")]
public class CountriesController : Controller
{
private readonly ICountryRepository countryRepository;
146
var city = await this.countryRepository.GetCityAsync(id.Value);
if (city == null)
{
return NotFound();
}
return View(city);
}
[HttpPost]
public async Task<IActionResult> EditCity(City city)
{
if (this.ModelState.IsValid)
{
var countryId = await this.countryRepository.UpdateCityAsync(city);
147
if (countryId != 0)
{
return this.RedirectToAction($"Details/{countryId}");
}
}
return this.View(city);
}
[HttpPost]
public async Task<IActionResult> AddCity(CityViewModel model)
{
if (this.ModelState.IsValid)
{
await this.countryRepository.AddCityAsync(model);
148
return this.RedirectToAction($"Details/{model.CountryId}");
}
return this.View(model);
}
return View(country);
}
149
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Country country)
{
if (ModelState.IsValid)
{
await this.countryRepository.CreateAsync(country);
return RedirectToAction(nameof(Index));
}
return View(country);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Country country)
{
150
if (ModelState.IsValid)
{
await this.countryRepository.UpdateAsync(country);
return RedirectToAction(nameof(Index));
}
return View(country);
}
await this.countryRepository.DeleteAsync(country);
return RedirectToAction(nameof(Index));
}
}
Index:
@model IEnumerable<Shop.Web.Data.Entities.Country>
151
@{
ViewData["Title"] = "Index";
}
<h2>Countries</h2>
<p>
<a asp-action="Create" class="btn btn-primary">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.NumberCities)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.NumberCities)
</td>
152
<td id="@item.Id">
<a asp-action="Edit" asp-route-id="@item.Id" class="btn btn-warning">Edit</a>
<a asp-action="Details" asp-route-id="@item.Id" class="btn btn-info">Details</a>
<a asp-action="Delete" asp-route-id="@item.Id" class="btn btn-danger" id="btnDelete">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
153
$(document).ready(function () {
var id = 0;
$('a[id*=btnDelete]').click(function () {
debugger;
id = $(this).parent()[0].id;
$("#deleteDialog").modal('show');
return false;
});
$("#btnNoDelete").click(function () {
$("#deleteDialog").modal('hide');
return false;
});
$("#btnYesDelete").click(function () {
window.location.href = '/Countries/Delete/' + id;
});
});
</script>
}
Create:
@model Shop.Web.Data.Entities.Country
@{
ViewData["Title"] = "Create";
}
154
<h2>Create</h2>
<h4>Country</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-success">Back to List</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Edit:
@model Shop.Web.Data.Entities.Country
@{
ViewData["Title"] = "Edit";
}
155
<h2>Edit</h2>
<h4>Country</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-success">Back to List</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Details:
@model Shop.Web.Data.Entities.Country
@{
156
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<div>
<h4>Country</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Name)
</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">Edit</a>
<a asp-action="AddCity" asp-route-id="@Model.Id" class="btn btn-info">Add City</a>
<a asp-action="Index" class="btn btn-success">Back to List</a>
</div>
<h4>Cities</h4>
@if (Model.Cities == null || Model.Cities.Count == 0)
{
<h5>No cities added yet</h5>
}
else
{
<table class="table">
<thead>
157
<tr>
<th>
@Html.DisplayNameFor(model => model.Cities.FirstOrDefault().Name)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Cities.OrderBy(c => c.Name))
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td id="@item.Id">
<a asp-action="EditCity" asp-route-id="@item.Id" class="btn btn-warning">Edit</a>
<a asp-action="DeleteCity" asp-route-id="@item.Id" class="btn btn-danger" id="btnDelete">Delete</a>
</td>
</tr>
}
</tbody>
</table>
}
158
<p>Do you want to delete the city?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" id="btnYesDelete">Delete</button>
<button type="button" class="btn btn-success" id="btnNoDelete">No</button>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$(document).ready(function () {
var id = 0;
$('a[id*=btnDelete]').click(function () {
debugger;
id = $(this).parent()[0].id;
$("#deleteDialog").modal('show');
return false;
});
$("#btnNoDelete").click(function () {
$("#deleteDialog").modal('hide');
return false;
});
$("#btnYesDelete").click(function () {
159
window.location.href = '/Countries/DeleteCity/' + id;
});
});
</script>
}
Add city:
@model Shop.Web.Models.CityViewModel
<h4>City</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="AddCity">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="CountryId" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-success">Back to List</a>
</div>
</form>
</div>
</div>
160
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Edit city:
@model Shop.Web.Data.Entities.City
<h4>City</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="EditCity">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
<a asp-action="Index" class="btn btn-success">Back to List</a>
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
161
}
[Required]
[Compare("Password")]
public string Confirm { get; set; }
[MaxLength(100, ErrorMessage = "The field {0} only can contain {1} characters length.")]
public string Address { get; set; }
[MaxLength(20, ErrorMessage = "The field {0} only can contain {1} characters length.")]
public string PhoneNumber { get; set; }
[Display(Name = "City")]
[Range(1, int.MaxValue, ErrorMessage = "You must select a city.")]
public int CityId { get; set; }
162
public IEnumerable<SelectListItem> Cities { get; set; }
[Display(Name = "Country")]
[Range(1, int.MaxValue, ErrorMessage = "You must select a country.")]
public int CountryId { get; set; }
IEnumerable<SelectListItem> GetComboCountries();
163
return list;
}
return list;
}
14. Change the register method in account controller (first inject ICountryRepository):
164
public IActionResult Register()
{
var model = new RegisterNewUserViewModel
{
Countries = this.countryRepository.GetComboCountries(),
Cities = this.countryRepository.GetComboCities(0)
};
return this.View(model);
}
[HttpPost]
public async Task<IActionResult> Register(RegisterNewUserViewModel model)
{
if (this.ModelState.IsValid)
{
var user = await this.userManager.FindByEmailAsync(model.Username);
if (user == null)
{
var city = await this.countryRepository.GetCityAsync(model.CityId);
165
var result = await this.userManager.CreateAsync(user, model.Password);
if (result != IdentityResult.Success)
{
this.ModelState.AddModelError(string.Empty, "The user couldn't be created.");
return this.View(model);
}
if (result2.Succeeded)
{
await this.userManager.AddToRoleAsync(user, "Customer");
return this.RedirectToAction("Index", "Home");
}
return this.View(model);
}
166
<div class="form-group">
<label asp-for="FirstName">First Name</label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="LastName">Last Name</label>
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="Username">Username</label>
<input asp-for="Username" class="form-control" />
<span asp-validation-for="Username" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="CountryId" class="control-label"></label>
<select asp-for="CountryId" asp-items="Model.Countries" class="form-control"></select>
<span asp-validation-for="CountryId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CityId" class="control-label"></label>
<select asp-for="CityId" asp-items="Model.Cities" class="form-control"></select>
<span asp-validation-for="CityId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Address">Address</label>
167
<input asp-for="Address" class="form-control" />
<span asp-validation-for="Address" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="PhoneNumber">Phone Number</label>
<input asp-for="PhoneNumber" class="form-control" />
<span asp-validation-for="PhoneNumber" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="Password">Password</label>
<input asp-for="Password" type="password" class="form-control" />
<span asp-validation-for="Password" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="Confirm">Confirm</label>
<input asp-for="Confirm" type="password" class="form-control" />
<span asp-validation-for="Confirm" class="text-warning"></span>
</div>
168
}
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$(document).ready(function () {
$("#CountryId").change(function () {
$("#CityId").empty();
$.ajax({
type: 'POST',
url: '@Url.Action("GetCitiesAsync")',
dataType: 'json',
data: { countryId: $("#CountryId").val() },
success: function (cities) {
debugger;
$("#CityId").append('<option value="0">(Select a city...)</option>');
$.each(cities, function (i, city) {
$("#CityId").append('<option value="'
+ city.id + '">'
+ city.name + '</option>');
});
},
error: function (ex) {
alert('Failed to retrieve cities.' + ex);
}
});
return false;
})
});
</script>
169
}
21. Now we’ll continue with the user modification. Please modify the model ChangeUserViewModel:
[Required]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[MaxLength(100, ErrorMessage = "The field {0} only can contain {1} characters length.")]
public string Address { get; set; }
[MaxLength(20, ErrorMessage = "The field {0} only can contain {1} characters length.")]
public string PhoneNumber { get; set; }
[Display(Name = "City")]
[Range(1, int.MaxValue, ErrorMessage = "You must select a city.")]
public int CityId { get; set; }
[Display(Name = "Country")]
[Range(1, int.MaxValue, ErrorMessage = "You must select a country.")]
public int CountryId { get; set; }
170
var user = await this.userHelper.GetUserByEmailAsync(this.User.Identity.Name);
var model = new ChangeUserViewModel();
if (user != null)
{
model.FirstName = user.FirstName;
model.LastName = user.LastName;
model.Address = user.Address;
model.PhoneNumber = user.PhoneNumber;
model.Cities = this.countryRepository.GetComboCities(model.CountryId);
model.Countries = this.countryRepository.GetComboCountries();
return this.View(model);
}
[HttpPost]
public async Task<IActionResult> ChangeUser(ChangeUserViewModel model)
{
171
if (this.ModelState.IsValid)
{
var user = await this.userHelper.GetUserByEmailAsync(this.User.Identity.Name);
if (user != null)
{
var city = await this.countryRepository.GetCityAsync(model.CityId);
user.FirstName = model.FirstName;
user.LastName = model.LastName;
user.Address = model.Address;
user.PhoneNumber = model.PhoneNumber;
user.CityId = model.CityId;
user.City = city;
return this.View(model);
}
172
23. Modify the view:
@model Shop.Web.Models.ChangeUserViewModel
@{
ViewData["Title"] = "Register";
}
<h2>Update User</h2>
<div class="row">
<div class="col-md-4 offset-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="FirstName">First Name</label>
<input asp-for="FirstName" class="form-control" />
<span asp-validation-for="FirstName" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="LastName">Last Name</label>
<input asp-for="LastName" class="form-control" />
<span asp-validation-for="LastName" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="CountryId" class="control-label"></label>
<select asp-for="CountryId" asp-items="Model.Countries" class="form-control"></select>
<span asp-validation-for="CountryId" class="text-danger"></span>
</div>
173
<div class="form-group">
<label asp-for="CityId" class="control-label"></label>
<select asp-for="CityId" asp-items="Model.Cities" class="form-control"></select>
<span asp-validation-for="CityId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Address">Address</label>
<input asp-for="Address" class="form-control" />
<span asp-validation-for="Address" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="PhoneNumber">Phone Number</label>
<input asp-for="PhoneNumber" class="form-control" />
<span asp-validation-for="PhoneNumber" class="text-warning"></span>
</div>
<div class="form-group">
<input type="submit" value="Update" class="btn btn-primary" />
<a asp-action="ChangePassword" class="btn btn-success">Change Password</a>
</div>
<div class="text-success">@ViewBag.UserMessage</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
174
$(document).ready(function () {
$("#CountryId").change(function () {
$("#CityId").empty();
$.ajax({
type: 'POST',
url: '@Url.Action("GetCities")',
dataType: 'json',
data: { countryId: $("#CountryId").val() },
success: function (cities) {
debugger;
$("#CityId").append('<option value="0">(Select a city...)</option>');
$.each(cities, function (i, city) {
$("#CityId").append('<option value="'
+ city.id + '">'
+ city.name + '</option>');
});
},
error: function (ex) {
alert('Failed to retrieve cities.' + ex);
}
});
return false;
})
});
</script>
}
175
Confirm Email Registration
1. First, change the setup project:
"Mail": {
"From": "youremail@gmail.com",
"Smtp": "smtp.gmail.com",
"Port": 587,
"Password": "yourpassword"
}
using MailKit.Net.Smtp;
using Microsoft.Extensions.Configuration;
using MimeKit;
177
var bodyBuilder = new BodyBuilder();
bodyBuilder.HtmlBody = body;
message.Body = bodyBuilder.ToMessageBody();
services.AddScoped<IMailHelper, MailHelper>();
178
public async Task<string> GenerateEmailConfirmationTokenAsync(User user)
{
return await this.userManager.GenerateEmailConfirmationTokenAsync(user);
}
9. Modify the register post method (first inject the IMailHelper in AccountController):
[HttpPost]
public async Task<IActionResult> Register(RegisterNewUserViewModel model)
{
if (this.ModelState.IsValid)
{
var user = await this.userHelper.GetUserByEmailAsync(model.Username);
if (user == null)
{
var city = await this.countryRepository.GetCityAsync(model.CityId);
179
City = city
};
return this.View(model);
}
180
...
<div class="form-group">
<input type="submit" value="Register New User" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div class="text-success">
<p>
@ViewBag.Message
</p>
</div>
@section Scripts {
...
181
var result = await this.userHelper.ConfirmEmailAsync(user, token);
if (!result.Succeeded)
{
return this.NotFound();
}
return View();
}
@{
ViewData["Title"] = "Confirm email";
}
<h2>@ViewData["Title"]</h2>
<div>
<p>
Thank you for confirming your email. Now you can login into system.
</p>
</div>
13. Drop the database (PM> drop-database) to ensure that all the users have a confirmed email.
182
Password Recovery
1. Modify the login view:
<div class="form-group">
<input type="submit" value="Login" class="btn btn-success" />
<a asp-action="Register" class="btn btn-primary">Register New User</a>
<a asp-action="RecoverPassword" class="btn btn-link">Forgot your password?</a>
</div>
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
[Required]
[DataType(DataType.Password)]
183
public string Password { get; set; }
[Required]
[DataType(DataType.Password)]
[Compare("Password")]
public string ConfirmPassword { get; set; }
[Required]
public string Token { get; set; }
}
184
{
return this.View();
}
[HttpPost]
public async Task<IActionResult> RecoverPassword(RecoverPasswordViewModel model)
{
if (this.ModelState.IsValid)
{
var user = await this.userHelper.GetUserByEmailAsync(model.Email);
if (user == null)
{
ModelState.AddModelError(string.Empty, "The email doesn't correspont to a registered user.");
return this.View(model);
}
return this.View(model);
}
185
public IActionResult ResetPassword(string token)
{
return View();
}
[HttpPost]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
var user = await this.userHelper.GetUserByEmailAsync(model.UserName);
if (user != null)
{
var result = await this.userHelper.ResetPasswordAsync(user, model.Token, model.Password);
if (result.Succeeded)
{
this.ViewBag.Message = "Password reset successful.";
return this.View();
}
@model Shop.Web.Models.RecoverPasswordViewModel
@{
ViewData["Title"] = "Recover Password";
186
}
<h2>Recover Password</h2>
<div class="row">
<div class="col-md-4 offset-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly"></div>
<div class="form-group">
<label asp-for="Email">Email</label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-warning"></span>
</div>
<div class="form-group">
<input type="submit" value="Recover password" class="btn btn-primary" />
<a asp-action="Login" class="btn btn-success">Back to login</a>
</div>
</form>
<div class="text-success">
<p>
@ViewBag.Message
</p>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
187
7. Add the view:
@model Shop.Web.Models.ResetPasswordViewModel
@{
ViewData["Title"] = "Reset Password";
}
<div class="row">
<div class="col-md-4 offset-md-4">
<form method="post">
<div asp-validation-summary="All"></div>
<input type="hidden" asp-for="Token" />
<div class="form-group">
<label asp-for="UserName">Email</label>
<input asp-for="UserName" class="form-control" />
<span asp-validation-for="UserName" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="Password">New password</label>
<input asp-for="Password" type="password" class="form-control" />
<span asp-validation-for="Password" class="text-warning"></span>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword">Confirm</label>
<input asp-for="ConfirmPassword" type="password" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-warning"></span>
188
</div>
<div class="form-group">
<input type="submit" value="Reset password" class="btn btn-primary" />
</div>
</form>
<div class="text-success">
<p>
@ViewBag.Message
</p>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
8. Test it.
<!DOCTYPE html>
<html>
189
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - Shop.Web </title>
<environment include="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link href="~/node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
<link rel="stylesheet" href="~/css/site.css" />
<link href="~/node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css" rel="stylesheet" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
<link href="~/node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css" rel="stylesheet" />
<link href="~/node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">Shop.Web</a>
190
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li>
<li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li>
@if (this.User.Identity.IsAuthenticated)
{
<li><a asp-area="" asp-controller="Products" asp-action="Index">Products</a></li>
@if (this.User.IsInRole("Admin"))
{
<li><a asp-area="" asp-controller="Countries" asp-action="Index">Countries</a></li>
}
<li><a asp-area="" asp-controller="Orders" asp-action="Index">Orders</a></li>
}
</ul>
<ul class="nav navbar-nav navbar-right">
@if (this.User.Identity.IsAuthenticated)
{
<li><a asp-area="" asp-controller="Account" asp-action="ChangeUser">@this.User.Identity.Name</a></li>
<li><a asp-area="" asp-controller="Account" asp-action="Logout">Logout</a></li>
}
else
{
<li><a asp-area="" asp-controller="Account" asp-action="Login">Login</a></li>
}
</ul>
</div>
</div>
</nav>
191
<partial name="_CookieConsentPartial" />
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment exclude="Development">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
</script>
<script src="~/node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js"></script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
192
</environment>
3. Test It!.
193
Improve the Seeder
1. Add the products images:
using System;
using System.Linq;
using System.Threading.Tasks;
using Entities;
using Helpers;
using Microsoft.AspNetCore.Identity;
await this.CheckRoles();
194
if (!this.context.Countries.Any())
{
await this.AddCountriesAndCitiesAsync();
}
// Add products
if (!this.context.Products.Any())
{
this.AddProduct("AirPods", 159, user);
this.AddProduct("Blackmagic eGPU Pro", 1199, user);
this.AddProduct("iPad Pro", 799, user);
this.AddProduct("iMac", 1398, user);
this.AddProduct("iPhone X", 749, user);
this.AddProduct("iWatch Series 4", 399, user);
this.AddProduct("Mac Book Air", 789, user);
this.AddProduct("Mac Book Pro", 1299, user);
this.AddProduct("Mac Mini", 708, user);
this.AddProduct("Mac Pro", 2300, user);
this.AddProduct("Magic Mouse", 47, user);
this.AddProduct("Magic Trackpad 2", 140, user);
this.AddProduct("USB C Multiport", 145, user);
this.AddProduct("Wireless Charging Pad", 67.67M, user);
await this.context.SaveChangesAsync();
}
}
private async Task<User> CheckUserAsync(string userName, string firstName, string lastName, string role)
{
195
// Add user
var user = await this.userHelper.GetUserByEmailAsync(userName);
if (user == null)
{
user = await this.AddUser(userName, firstName, lastName, role);
}
return user;
}
private async Task<User> AddUser(string userName, string firstName, string lastName, string role)
{
var user = new User
{
FirstName = firstName,
LastName = lastName,
Email = userName,
UserName = userName,
Address = "Calle Luna Calle Sol",
PhoneNumber = "350 634 2747",
CityId = this.context.Countries.FirstOrDefault().Cities.FirstOrDefault().Id,
City = this.context.Countries.FirstOrDefault().Cities.FirstOrDefault()
};
196
{
throw new InvalidOperationException("Could not create the user in seeder");
}
197
await this.context.SaveChangesAsync();
}
198
3. Drop the database and test it.
@model IEnumerable<Shop.Web.Data.Entities.OrderDetailTemp>
@{
ViewData["Title"] = "Create";
}
<h2>Create</h2>
<p>
<a asp-action="AddProduct" class="btn btn-success">Add Product</a>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#confirmDialog">Confirm Order</button>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Product.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th>
199
@Html.DisplayNameFor(model => model.Quantity)
</th>
<th>
@Html.DisplayNameFor(model => model.Value)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Product.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.Quantity)
</td>
<td>
@Html.DisplayFor(modelItem => item.Value)
</td>
<td>
<a asp-action="Increase" asp-route-id="@item.Id" class="btn btn-warning"><i class="fa fa-plus"></i></a>
<a asp-action="Decrease" asp-route-id="@item.Id" class="btn btn-info"><i class="fa fa-minus"></i></a>
<button data-id="@item.Id" class="btn btn-danger deleteItem" data-toggle="modal" data-
target="#deleteDialog">Delete</button>
</td>
</tr>
}
200
</tbody>
</table>
<!--Delete Item-->
<div class="modal fade" id="deleteDialog" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Delete Item</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
201
</button>
</div>
<div class="modal-body">
<p>Do you want to delete the product from order?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" id="btnYesDelete">Delete</button>
</div>
</div>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script type="text/javascript">
$(document).ready(function () {
// Confirm Order
$("#btnYes").click(function () {
window.location.href = '/Orders/ConfirmOrder';
});
// Delete item
var item_to_delete;
$('.deleteItem').click((e) => {
item_to_delete = e.currentTarget.dataset.id;
});
$("#btnYesDelete").click(function () {
window.location.href = '/Orders/DeleteItem/' + item_to_delete;
202
});
});
</script>
}
2. Test it.
@model IEnumerable<Shop.Web.Data.Entities.Product>
@{
ViewData["Title"] = "Index";
}
<p>
<a asp-action="Create" class="btn btn-primary"><i class="glyphicon glyphicon-plus"></i> Create New</a>
</p>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Products</h3>
203
</div>
<div class="panel-body">
<table class="table table-hover table-responsive table-striped" id="ProductsTable">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th>
@Html.DisplayNameFor(model => model.ImageUrl)
</th>
<th>
@Html.DisplayNameFor(model => model.LastPurchase)
</th>
<th>
@Html.DisplayNameFor(model => model.LastSale)
</th>
<th>
@Html.DisplayNameFor(model => model.IsAvailabe)
</th>
<th>
@Html.DisplayNameFor(model => model.Stock)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
204
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@if (!string.IsNullOrEmpty(item.ImageUrl))
{
<img src="@Url.Content(item.ImageUrl)" alt="Image" style="width:75px;height:75px;max-width: 100%;
height: auto;" />
}
</td>
<td>
@Html.DisplayFor(modelItem => item.LastPurchase)
</td>
<td>
@Html.DisplayFor(modelItem => item.LastSale)
</td>
<td>
@Html.DisplayFor(modelItem => item.IsAvailabe)
</td>
<td>
@Html.DisplayFor(modelItem => item.Stock)
</td>
<td>
<a asp-action="Edit" class="btn btn-default" asp-route-id="@item.Id"><i class="glyphicon glyphicon-
pencil"></i> </a>
205
<a asp-action="Details" class="btn btn-default" asp-route-id="@item.Id"><i class="glyphicon glyphicon-list">
</i> </a>
<a asp-action="Delete" class="btn btn-danger" asp-route-id="@item.Id"><i class="glyphicon glyphicon-
trash"></i> </a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
@section Scripts {
<script src="//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function () {
$('#ProductsTable').DataTable();
});
</script>
}
2. Test it.
User Management
1. Modify the User entity (in Web.Data.Entities):
using Microsoft.AspNetCore.Identity;
206
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
[MaxLength(100, ErrorMessage = "The field {0} only can contain {1} characters length.")]
public string Address { get; set; }
[NotMapped]
[Display(Name = "Is Admin?")]
public bool IsAdmin { get; set; }
}
207
2. Add this methods to IUserHelper (in Web.Helpers):
Task<List<User>> GetAllUsersAsync();
208
{
var users = await this.userHelper.GetAllUsersAsync();
foreach (var user in users)
{
var myUser = await this.userHelper.GetUserByIdAsync(user.Id);
if (myUser != null)
{
user.IsAdmin = await this.userHelper.IsUserInRoleAsync(myUser, "Admin");
}
}
return this.View(users);
}
209
{
if (string.IsNullOrEmpty(id))
{
return NotFound();
}
await this.userHelper.DeleteUserAsync(user);
return this.RedirectToAction(nameof(Index));
}
210
4. Finally add the Index view:
@model IEnumerable<Shop.Web.Data.Entities.User>
@{
ViewData["Title"] = "Index";
}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Users</h3>
</div>
<div class="panel-body">
<table class="table table-hover table-responsive table-striped" id="UsersTable">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.FirstName)
</th>
<th>
@Html.DisplayNameFor(model => model.LastName)
</th>
<th>
@Html.DisplayNameFor(model => model.Email)
</th>
<th>
211
@Html.DisplayNameFor(model => model.Address)
</th>
<th>
@Html.DisplayNameFor(model => model.PhoneNumber)
</th>
<th>
@Html.DisplayNameFor(model => model.City.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.IsAdmin)
</th>
<th>
@Html.DisplayNameFor(model => model.EmailConfirmed)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.FirstName)
</td>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
212
</td>
<td>
@Html.DisplayFor(modelItem => item.PhoneNumber)
</td>
<td>
@Html.DisplayFor(modelItem => item.City.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.IsAdmin)
</td>
<td>
@Html.DisplayFor(modelItem => item.EmailConfirmed)
</td>
<td>
<button data-id="@item.Id" class="btn btn-danger deleteItem" data-toggle="modal" data-
target="#deleteDialog">Delete</button>
@if (item.IsAdmin)
{
<a asp-action="AdminOff" asp-route-id="@item.Id" class="btn btn-warning">Admin Off</a>
}
else
{
<a asp-action="AdminOn" asp-route-id="@item.Id" class="btn btn-primary">Admin On</a>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
213
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script src="//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
<script type="text/javascript">
$(document).ready(function () {
$('#UsersTable').DataTable();
// Delete item
var item_to_delete;
214
$('.deleteItem').click((e) => {
item_to_delete = e.currentTarget.dataset.id;
});
$("#btnYesDelete").click(function () {
window.location.href = '/Account/DeleteUser/' + item_to_delete;
});
});
</script>
}
@if (this.User.Identity.IsAuthenticated)
{
<li><a asp-area="" asp-controller="Products" asp-action="Index">Products</a></li>
@if (this.User.IsInRole("Admin"))
{
<li><a asp-area="" asp-controller="Countries" asp-action="Index">Countries</a></li>
<li><a asp-area="" asp-controller="Account" asp-action="Index">Users</a></li>
}
<li><a asp-area="" asp-controller="Orders" asp-action="Index">Orders</a></li>
}
6. Test it.
215
public class TokenRequest
{
public string Username { get; set; }
using System;
using Newtonsoft.Json;
[JsonProperty("expiration")]
public DateTime Expiration { get; set; }
}
1. In the ApiService add the methods GetTokenAsync and overload the method GetListAsync:
216
var client = new HttpClient
{
BaseAddress = new Uri(urlBase),
};
if (!response.IsSuccessStatusCode)
{
return new Response
{
IsSuccess = false,
Message = result,
};
}
217
Message = ex.Message
};
}
}
if (!response.IsSuccessStatusCode)
{
return new Response
{
IsSuccess = false,
Message = result,
};
}
218
var token = JsonConvert.DeserializeObject<TokenResponse>(result);
return new Response
{
IsSuccess = true,
Result = token
};
}
catch (Exception ex)
{
return new Response
{
IsSuccess = false,
Message = ex.Message
};
}
}
219
Text="Email">
</Label>
<Entry
Keyboard="Email"
Placeholder="Enter your email..."
Text="{Binding Email}">
</Entry>
<Label
Text="Password">
</Label>
<Entry
IsPassword="True"
Placeholder="Enter your password..."
Text="{Binding Password}">
</Entry>
<ActivityIndicator
IsRunning="{Binding IsRunning}">
</ActivityIndicator>
<Button
Command="{Binding LoginCommand}"
IsEnabled="{Binding IsEnabled}"
Text="Login">
</Button>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
220
4. Add the new value into resource dictionary:
using Common.Services;
using GalaSoft.MvvmLight.Command;
using Shop.Common.Models;
using System.Windows.Input;
using Views;
using Xamarin.Forms;
221
get => this.isEnabled;
set => this.SetValue(ref this.isEnabled, value);
}
public LoginViewModel()
{
this.apiService = new ApiService();
this.IsEnabled = true;
this.Email = "jzuluaga55@gmail.com";
this.Password = "123456";
}
if (string.IsNullOrEmpty(this.Password))
{
await Application.Current.MainPage.DisplayAlert("Error", "You must enter a password.", "Accept");
return;
}
222
this.IsRunning = true;
this.IsEnabled = false;
this.IsRunning = false;
this.IsEnabled = true;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert("Error", "Email or password incorrect.", "Accept");
return;
}
223
6. Test it.
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
response.Message,
"Accept");
this.IsRefreshing = false;
return;
}
224
}
8. Test it.
225
xmlns:pages="clr-namespace:Shop.UIForms.Views"
x:Class="Shop.UIForms.Views.MasterPage">
<MasterDetailPage.Master>
<pages:MenuPage/>
</MasterDetailPage.Master>
<MasterDetailPage.Detail>
<NavigationPage x:Name="Navigator">
<x:Arguments>
<pages:ProductsPage/>
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Detail>
</MasterDetailPage>
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MasterPage : MasterDetailPage
{
public MasterPage()
{
InitializeComponent();
}
226
}
7. Add icons for: About, Setup, Exit and an image for the solution. I recommend Android Asset Studio. And add those icons in
their corresponding folder by the platform.
227
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Shop.UIForms.Views.AboutPage"
Title="About">
<ContentPage.Content>
<StackLayout>
<Label Text="About"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
228
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Shop.UIForms.Views.MenuPage"
BackgroundColor="Gainsboro"
BindingContext="{Binding Main, Source={StaticResource Locator}}"
Title="Menu">
<ContentPage.Content>
<StackLayout
Padding="10">
<Image
HeightRequest="150"
Source="shop">
</Image>
<ListView
ItemsSource="{Binding Menus}"
HasUnevenRows="True"
SeparatorVisibility="None">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
HeightRequest="50"
Source="{Binding Icon}"
WidthRequest="50">
</Image>
<Label
Grid.Column="1"
229
FontAttributes="Bold"
VerticalOptions="Center"
TextColor="White"
Text="{Binding Title}">
</Label>
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
230
Text="Email">
</Label>
<Entry
Keyboard="Email"
Placeholder="Enter your email..."
Text="{Binding Email}">
</Entry>
<Label
Text="Password">
</Label>
<Entry
IsPassword="True"
Placeholder="Enter your password..."
Text="{Binding Password}">
</Entry>
<ActivityIndicator
IsRunning="{Binding IsRunning}"
VerticalOptions="CenterAndExpand">
</ActivityIndicator>
<Button
BackgroundColor="Navy"
BorderRadius="23"
Command="{Binding LoginCommand}"
HeightRequest="46"
IsEnabled="{Binding IsEnabled}"
Text="Login"
TextColor="White">
</Button>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
231
13. Test it:
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SelectMenuCommand}"/>
</Grid.GestureRecognizers>
<Grid.ColumnDefinitions>
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;
using Views;
using Xamarin.Forms;
switch (this.PageName)
{
case "AboutPage":
await App.Navigator.PushAsync(new AboutPage());
break;
case "SetupPage":
232
await App.Navigator.PushAsync(new SetupPage());
break;
default:
MainViewModel.GetInstance().Login = new LoginViewModel();
Application.Current.MainPage = new NavigationPage(new LoginPage());
break;
}
}
}
new Menu
233
{
Icon = "ic_phonelink_setup",
PageName = "SetupPage",
Title = "Setup"
},
new Menu
{
Icon = "ic_exit_to_app",
PageName = "LoginPage",
Title = "Close session"
}
};
234
19. Add the property in App:
App.Master.IsPresented = false;
[JsonProperty("lastPurchase")]
public DateTime? LastPurchase { get; set; }
[JsonProperty("lastSale")]
public DateTime? LastSale { get; set; }
And implementation:
235
public async Task<T> CreateAsync(T entity)
{
await this.context.Set<T>().AddAsync(entity);
await SaveAllAsync();
return entity;
}
[HttpPost]
public async Task<IActionResult> PostProduct([FromBody] Common.Models.Product product)
{
if (!ModelState.IsValid)
{
return this.BadRequest(ModelState);
}
236
{
IsAvailabe = product.IsAvailabe,
LastPurchase = product.LastPurchase,
LastSale = product.LastSale,
Name = product.Name,
Price = product.Price,
Stock = product.Stock,
User = user
};
[HttpPut("{id}")]
public async Task<IActionResult> PutProduct([FromRoute] int id, [FromBody] Common.Models.Product product)
{
if (!ModelState.IsValid)
{
return this.BadRequest(ModelState);
}
if (id != product.Id)
{
return BadRequest();
}
237
//TODO: Upload images
oldProduct.IsAvailabe = product.IsAvailabe;
oldProduct.LastPurchase = product.LastPurchase;
oldProduct.LastSale = product.LastSale;
oldProduct.Name = product.Name;
oldProduct.Price = product.Price;
oldProduct.Stock = product.Stock;
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteProduct([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return this.BadRequest(ModelState);
}
await this.productRepository.DeleteAsync(product);
return Ok(product);
}
5. Test it in PostMan.
238
6. Publish the changes in Azure.
239
{
IsSuccess = false,
Message = answer,
};
}
240
<ToolbarItem Icon="ic_action_add_circle" Command="{Binding AddProductCommand}"/>
</ContentPage.ToolbarItems>
<ContentPage.Content>
...
241
<Label
Grid.Column="0"
Grid.Row="0"
Text="Name"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="0"
Placeholder="Product name..."
Text="{Binding Name}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="1"
Text="Price"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="1"
Keyboard="Numeric"
Placeholder="Product price..."
Text="{Binding Price}">
</Entry>
</Grid>
<ActivityIndicator
IsRunning="{Binding IsRunning}"
VerticalOptions="CenterAndExpand">
</ActivityIndicator>
<Button
BackgroundColor="Navy"
242
BorderRadius="23"
Command="{Binding SaveCommand}"
HeightRequest="46"
IsEnabled="{Binding IsEnabled}"
Text="Save"
TextColor="White">
</Button>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
5. Modify the LoginViewModel to storage the user email when this is logged in:
using System.Windows.Input;
using Common.Models;
using Common.Services;
using GalaSoft.MvvmLight.Command;
243
using Xamarin.Forms;
public AddProductViewModel()
{
this.apiService = new ApiService();
this.Image = "noImage";
244
this.IsEnabled = true;
}
if (string.IsNullOrEmpty(this.Price))
{
await Application.Current.MainPage.DisplayAlert("Error", "You must enter a product price.", "Accept");
return;
}
this.IsRunning = true;
this.IsEnabled = false;
245
Price = price,
User = new User { UserName = MainViewModel.GetInstance().UserEmail }
};
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert("Error", response.Message, "Accept");
return;
}
this.IsRunning = false;
this.IsEnabled = true;
await App.Navigator.PopAsync();
}
}
7. Modify the MainViewModel adding the property AddProduct, AddProductCommand and GoAddProduct method:
246
public ICommand AddProductCommand => new RelayCommand(this.GoAddProduct);
…
private async void GoAddProduct()
{
this.AddProduct = new AddProductViewModel();
await App.Navigator.PushAsync(new AddProductPage());
}
[JsonProperty("imageFullPath")]
public string ImageFullPath { get; set; }
12. Now we implement the update and delete operations. Add this methods to the API controller:
247
{
try
{
var request = JsonConvert.SerializeObject(model);
var content = new StringContent(request, Encoding.UTF8, "application/json");
var client = new HttpClient
{
BaseAddress = new Uri(urlBase)
};
248
return new Response
{
IsSuccess = false,
Message = ex.Message,
};
}
}
249
Message = answer,
};
}
250
<Image
HeightRequest="150"
Source="{Binding Product.ImageFullPath}">
</Image>
<Label
FontSize="Micro"
HorizontalOptions="Center"
Text="Tap the image to change it...">
</Label>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label
Grid.Column="0"
Grid.Row="0"
Text="Name"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="0"
Placeholder="Product name..."
Text="{Binding Product.Name}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="1"
Text="Price"
VerticalOptions="Center">
</Label>
251
<Entry
Grid.Column="1"
Grid.Row="1"
Keyboard="Numeric"
Placeholder="Product price..."
Text="{Binding Product.Price}">
</Entry>
</Grid>
<ActivityIndicator
IsRunning="{Binding IsRunning}"
VerticalOptions="CenterAndExpand">
</ActivityIndicator>
<StackLayout
Orientation="Horizontal">
<Button
BackgroundColor="Navy"
BorderRadius="23"
Command="{Binding SaveCommand}"
HeightRequest="46"
HorizontalOptions="FillAndExpand"
IsEnabled="{Binding IsEnabled}"
Text="Save"
TextColor="White">
</Button>
<Button
BackgroundColor="Red"
BorderRadius="23"
Command="{Binding DeleteCommand}"
HeightRequest="46"
HorizontalOptions="FillAndExpand"
IsEnabled="{Binding IsEnabled}"
Text="Delete"
252
TextColor="White">
</Button>
</StackLayout>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
using System.Windows.Input;
using Common.Models;
using GalaSoft.MvvmLight.Command;
using Views;
…
private List<Product> myProducts;
private ObservableCollection<ProductItemViewModel> products;
…
253
public ObservableCollection<ProductItemViewModel> Products
{
get => this.products;
set => this.SetValue(ref this.products, value);
}
…
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
response.Message,
"Accept");
return;
}
this.myProducts = (List<Product>)response.Result;
this.RefresProductsList();
}
254
this.myProducts.Add(product);
this.RefresProductsList();
}
this.RefresProductsList();
}
255
.ToList());
}
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding SelectProductCommand}"/>
</Grid.GestureRecognizers>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image
Grid.Column="0"
Source="{Binding ImageFullPath}"
WidthRequest="100">
</Image>
<StackLayout
Grid.Column="1"
VerticalOptions="Center">
<Label
FontAttributes="Bold"
FontSize="Medium"
Text="{Binding Name}"
TextColor="Black">
</Label>
<Label
Text="{Binding Price, StringFormat='Price: {0:C2}'}"
TextColor="Navy">
</Label>
256
<Label
Text="{Binding Stock, StringFormat='Stock: {0:N2}'}"
TextColor="Black">
</Label>
</StackLayout>
<Image
Grid.Column="2"
Source="ic_action_chevron_right">
</Image>
</Grid>
using System.Linq;
using System.Windows.Input;
using Common.Models;
using Common.Services;
using GalaSoft.MvvmLight.Command;
using Xamarin.Forms;
257
get => this.isRunning;
set => this.SetValue(ref this.isRunning, value);
}
if (this.Product.Price <= 0)
{
await Application.Current.MainPage.DisplayAlert("Error", "The price must be a number greather than zero.", "Accept");
258
return;
}
this.IsRunning = true;
this.IsEnabled = false;
this.IsRunning = false;
this.IsEnabled = true;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert("Error", response.Message, "Accept");
return;
}
259
var confirm = await Application.Current.MainPage.DisplayAlert("Confirm", "Are you sure to delete the product?", "Yes", "No");
if (!confirm)
{
return;
}
this.IsRunning = true;
this.IsEnabled = false;
this.IsRunning = false;
this.IsEnabled = true;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert("Error", response.Message, "Accept");
return;
}
MainViewModel.GetInstance().Products.DeleteProductInList(this.Product.Id);
await App.Navigator.PopAsync();
}
}
260
19. Add the property in the MainViewModel:
2. Add the folder Helpers (in Common.Helpers), and inside it, add the class Settings:
using Plugin.Settings;
using Plugin.Settings.Abstractions;
261
public static string UserEmail
{
get => AppSettings.GetValueOrDefault(userEmail, stringDefault);
set => AppSettings.AddOrUpdateValue(userEmail, value);
}
<Entry
IsPassword="True"
Placeholder="Enter your password..."
Text="{Binding Password}">
</Entry>
<StackLayout
HorizontalOptions="Center"
Orientation="Horizontal">
<Label
Text="Rememberme in this device"
262
VerticalOptions="Center">
</Label>
<Switch
IsToggled="{Binding IsRemember}">
</Switch>
</StackLayout>
<ActivityIndicator
IsRunning="{Binding IsRunning}"
VerticalOptions="CenterAndExpand">
</ActivityIndicator>
Settings.IsRemember = this.IsToggled;
Settings.UserEmail = this.Email;
Settings.UserPassword = this.Password;
Settings.Token = JsonConvert.SerializeObject(token);
263
Application.Current.MainPage = new MasterPage();
public App()
{
InitializeComponent();
if (Settings.IsRemember)
{
var token = JsonConvert.DeserializeObject<TokenResponse>(Settings.Token);
if (token.Expiration > DateTime.Now)
{
var mainViewModel = MainViewModel.GetInstance();
mainViewModel.Token = token;
mainViewModel.UserEmail = Settings.UserEmail;
mainViewModel.UserPassword = Settings.UserPassword;
mainViewModel.Products = new ProductsViewModel();
this.MainPage = new MasterPage();
return;
}
}
default:
Settings.IsRemember = false;
264
Settings.Token = string.Empty;
Settings.UserEmail = string.Empty;
Settings.UserPassword = string.Empty;
7. Test it.
2. In shared forms project add the folder Resources and inside it, add the resource call Resource, add some literals and
translate with the ResX Manager tool. The default resource language must be Public, the others in no code generation.
3. In shared forms project add the folder Interfaces, inside it, add the interface ILocalize.
using System.Globalization;
using System;
266
LocaleCode = "";
}
}
5. In the same folder add the class Languages with the literals.
using Interfaces;
using Resources;
using Xamarin.Forms;
267
6. Implement the interface in iOS in the folder Implementations.
[assembly: Xamarin.Forms.Dependency(typeof(Shop.UIForms.iOS.Implementations.Localize))]
namespace Shop.UIForms.iOS.Implementations
{
using System.Globalization;
using System.Threading;
using Foundation;
using Helpers;
using Interfaces;
268
{
var fallback = ToDotnetFallbackLanguage(new PlatformCulture(netLanguage));
ci = new System.Globalization.CultureInfo(fallback);
}
catch (CultureNotFoundException e2)
{
// iOS language not valid .NET culture, falling back to English
ci = new System.Globalization.CultureInfo("en");
}
}
return ci;
}
269
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
}
return netLanguage;
}
return netLanguage;
}
}
}
<key>CFBundleLocalizations</key>
<array>
<string>es</string>
270
<string>pt</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
[assembly: Xamarin.Forms.Dependency(typeof(Shop.UIForms.Droid.Implementations.Localize))]
namespace Shop.UIForms.Droid.Implementations
{
using Helpers;
using Interfaces;
using System.Globalization;
using System.Threading;
271
try
{
var fallback = ToDotnetFallbackLanguage(new PlatformCulture(netLanguage));
ci = new System.Globalization.CultureInfo(fallback);
}
catch (CultureNotFoundException)
{
// iOS language not valid .NET culture, falling back to English
ci = new System.Globalization.CultureInfo("en");
}
}
return ci;
}
272
netLanguage = "id-ID"; // correct code for .NET
break;
case "gsw-CH": // "Schwiizertüütsch (Swiss German)" not supported .NET culture
netLanguage = "de-CH"; // closest supported
break;
// add more application-specific cases here (if required)
// ONLY use cultures that have been tested and known to work
}
return netLanguage;
}
if (string.IsNullOrEmpty(this.Email))
{
await Application.Current.MainPage.DisplayAlert(Languages.Error, Languages.EmailMessage, Languages.Accept);
273
return;
}
11. Now to translate literals directly in the XAML add the class TranslateExtension in folder Helpers:
using Interfaces;
using System;
using System.Globalization;
using System.Reflection;
using System.Resources;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
[ContentProperty("Text")]
public class TranslateExtension : IMarkupExtension
{
private readonly CultureInfo ci;
private const string ResourceId = "Shop.UIForms.Resources.Resource";
private static readonly Lazy<ResourceManager> ResMgr =
new Lazy<ResourceManager>(() => new ResourceManager(
ResourceId,
typeof(TranslateExtension).GetTypeInfo().Assembly));
public TranslateExtension()
{
ci = DependencyService.Get<ILocalize>().GetCurrentCultureInfo();
}
274
public object ProvideValue(IServiceProvider serviceProvider)
{
if (Text == null)
{
return "";
}
if (translation == null)
{
#if DEBUG
throw new ArgumentException(
string.Format(
"Key '{0}' was not found in resources '{1}' for culture '{2}'.",
Text, ResourceId, ci.Name), "Text");
#else
translation = Text; // returns the key, which GETS DISPLAYED TO THE USER
#endif
}
return translation;
}
}
275
13. And add the properties in Languages class:
using Interfaces;
using Resources;
using Xamarin.Forms;
276
public static string EmailMEmailessage => Resource.Email;
if (string.IsNullOrEmpty(this.Email))
{
await Application.Current.MainPage.DisplayAlert(
Languages.Error,
Languages.EmailMessage,
Languages.Accept);
return;
}
if (string.IsNullOrEmpty(this.Password))
{
await Application.Current.MainPage.DisplayAlert(
Languages.Error,
Languages.PasswordMessage,
Languages.Accept);
277
return;
return;
}
this.IsRunning = true;
this.IsEnabled = false;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
Languages.Error,
Languages.EmailOrPasswordIncorrect,
Languages.Accept);
return;
return;
}
278
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:i18n="clr-namespace:Shop.UIForms.Helpers"
x:Class="Shop.UIForms.Views.LoginPage"
BindingContext="{Binding Main, Source={StaticResource Locator}}"
Title="{i18n:Translate Login}">
<ContentPage.Content>
<ScrollView
BindingContext="{Binding Login}">
<StackLayout
Padding="10">
<Image
HeightRequest="150"
Source="shop.png">
</Image>
<Label
Text="{i18n:Translate Email}">
</Label>
<Entry
Keyboard="{i18n:Translate Email}"
Placeholder="{i18n:Translate EmailPlaceHolder}"
Text="{Binding Email}">
</Entry>
<Label
Text="{i18n:Translate Password}">
</Label>
<Entry
IsPassword="True"
Placeholder="{i18n:Translate PasswordPlaceHolder}"
Text="{Binding Password}">
</Entry>
<StackLayout
279
HorizontalOptions="Center"
Orientation="Horizontal">
<Label
Text="{i18n:Translate Remember}"
VerticalOptions="Center">
</Label>
<Switch
IsToggled="{Binding IsToggled}">
</Switch>
</StackLayout>
<ActivityIndicator
IsRunning="{Binding IsRunning}"
VerticalOptions="CenterAndExpand">
</ActivityIndicator>
<Button
BackgroundColor="Navy"
BorderRadius="23"
Command="{Binding LoginCommand}"
HeightRequest="46"
IsEnabled="{Binding IsEnabled}"
Text="{i18n:Translate Login}"
TextColor="White">
</Button>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
280
Acceding To Camera and Gallery in Xamarin Forms
1. Change the AddProductPage:
<Image
HeightRequest="150"
Source="{Binding ImageSource}">
<Image.GestureRecognizers>
<TapGestureRecognizer Command="{Binding ChangeImageCommand}"/>
</Image.GestureRecognizers>
</Image>
this.ImageSource = "noImage";
281
protected override void OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(savedInstanceState);
CrossCurrentActivity.Current.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
}
282
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application android:label="ShopPrep.UIForms.Android">
<provider android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
</manifest>
7. Add the folder xml inside Resources and inside it, add the file_paths.xml:
<key>NSCameraUsageDescription</key>
<string>This app needs access to the camera to take photos.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to photos.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs access to the photo gallery.</string>
283
9. Add the attribute in AddProductViewModel:
if (source == "Cancel")
{
this.file = null;
return;
}
284
Directory = "Sample",
Name = "test.jpg",
PhotoSize = PhotoSize.Small,
}
);
}
else
{
this.file = await CrossMedia.Current.PickPhotoAsync();
}
if (this.file != null)
{
this.ImageSource = ImageSource.FromStream(() =>
{
var stream = file.GetStream();
return stream;
});
}
}
using System.IO;
285
public static bool UploadPhoto(MemoryStream stream, string folder, string name)
{
try
{
stream.Position = 0;
var path = Path.Combine(Directory.GetCurrentDirectory(), folder , name);
File.WriteAllBytes(path, stream.ToArray());
}
catch
{
return false;
}
return true;
}
}
[HttpPost]
public async Task<IActionResult> PostProduct([FromBody] Common.Models.Product product)
{
if (!ModelState.IsValid)
{
return this.BadRequest(ModelState);
}
286
if (user == null)
{
return this.BadRequest("Invalid user");
}
if (response)
{
imageUrl = fullPath;
}
}
287
var newProduct = await this.repository.AddProductAsync(entityProduct);
return Ok(newProduct);
}
using System.IO;
this.IsRunning = true;
this.IsEnabled = false;
288
var product = new Product
{
IsAvailabe = true,
Name = this.Name,
Price = price,
UserEmail = MainViewModel.GetInstance().UserEmail,
ImageArray = imageArray
};
6. Test it locally.
using Data;
using Microsoft.AspNetCore.Mvc;
[Route("api/[Controller]")]
public class CountriesController : Controller
{
private readonly ICountryRepository countryRepository;
[HttpGet]
289
public IActionResult GetCountries()
{
return Ok(this.countryRepository.GetCountriesWithCities());
}
using Newtonsoft.Json;
[JsonProperty("name")]
public string Name { get; set; }
}
And:
using System.Collections.Generic;
using Newtonsoft.Json;
290
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("cities")]
public List<City> Cities { get; set; }
[JsonProperty("numberCities")]
public int NumberCities { get; set; }
}
291
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="0"
Placeholder="Enter your first name..."
Text="{Binding FirstName}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="1"
Text="Last name"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="1"
Placeholder="Enter your last name..."
Text="{Binding LastName}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="2"
Text="Email"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="2"
Keyboard="Email"
Placeholder="Enter your email..."
Text="{Binding Email}">
292
</Entry>
<Label
Grid.Column="0"
Grid.Row="3"
Text="Country"
VerticalOptions="Center">
</Label>
<Picker
Grid.Column="1"
Grid.Row="3"
ItemDisplayBinding="{Binding Name}"
ItemsSource="{Binding Countries}"
SelectedItem="{Binding Country}"
Title="Select a country...">
</Picker>
<Label
Grid.Column="0"
Grid.Row="4"
Text="City"
VerticalOptions="Center">
</Label>
<Picker
Grid.Column="1"
Grid.Row="4"
ItemDisplayBinding="{Binding Name}"
ItemsSource="{Binding Cities}"
SelectedItem="{Binding City}"
Title="Select a city...">
</Picker>
<Label
Grid.Column="0"
Grid.Row="5"
293
Text="Address"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="5"
Keyboard="Email"
Placeholder="Enter your address..."
Text="{Binding Address}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="6"
Text="Pohone"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="6"
Keyboard="Telephone"
Placeholder="Enter your phone number..."
Text="{Binding Phone}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="7"
Text="Password"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="7"
294
IsPassword="True"
Placeholder="Enter your password..."
Text="{Binding Password}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="8"
Text="Password confirm"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="8"
IsPassword="True"
Placeholder="Enter your password confirm..."
Text="{Binding Confirm}">
</Entry>
</Grid>
<ActivityIndicator
IsRunning="{Binding IsRunning}"
VerticalOptions="CenterAndExpand">
</ActivityIndicator>
<Button
BackgroundColor="Navy"
BorderRadius="23"
Command="{Binding RegisterCommand}"
HeightRequest="46"
HorizontalOptions="FillAndExpand"
IsEnabled="{Binding IsEnabled}"
Text="Register New User"
TextColor="White">
</Button>
295
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
using System.Collections.ObjectModel;
using System.Windows.Input;
using Common.Models;
using GalaSoft.MvvmLight.Command;
296
public string Confirm { get; set; }
297
public bool IsEnabled
{
get => this.isEnabled;
set => this.SetValue(ref this.isEnabled, value);
}
public RegisterViewModel()
{
this.IsEnabled = true;
}
<StackLayout
Orientation="Horizontal">
<Button
BackgroundColor="Navy"
BorderRadius="23"
Command="{Binding LoginCommand}"
HeightRequest="46"
298
HorizontalOptions="FillAndExpand"
IsEnabled="{Binding IsEnabled}"
Text="{i18n:Translate Login}"
TextColor="White">
</Button>
<Button
BackgroundColor="Purple"
BorderRadius="23"
Command="{Binding RegisterCommand}"
HeightRequest="46"
HorizontalOptions="FillAndExpand"
IsEnabled="{Binding IsEnabled}"
Text="{i18n:Translate RegisterNewUser}"
TextColor="White">
</Button>
</StackLayout>
...
299
private readonly ApiService apiService;
…
public Country Country
{
get => this.country;
set
{
this.SetValue(ref this.country, value);
this.Cities = new ObservableCollection<City>(this.Country.Cities.OrderBy(c => c.Name));
}
}
…
public RegisterViewModel()
{
this.apiService = new ApiService();
this.IsEnabled = true;
this.LoadCountries();
}
…
private async void LoadCountries()
{
this.IsRunning = true;
this.IsEnabled = false;
this.IsRunning = false;
this.IsEnabled = true;
300
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
response.Message,
"Accept");
return;
}
using System;
using System.Net.Mail;
return true;
}
catch (FormatException)
301
{
return false;
}
}
}
using System.ComponentModel.DataAnnotations;
[Required]
public string LastName { get; set; }
[Required]
public string Email { get; set; }
[Required]
public string Address { get; set; }
[Required]
public string Phone { get; set; }
[Required]
public string Password { get; set; }
[Required]
public int CityId { get; set; }
302
}
using System.Linq;
using System.Threading.Tasks;
using Common.Models;
using Data;
using Helpers;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
[Route("api/[Controller]")]
public class AccountController : Controller
{
private readonly IUserHelper userHelper;
private readonly ICountryRepository countryRepository;
private readonly IMailHelper mailHelper;
public AccountController(
IUserHelper userHelper,
ICountryRepository countryRepository,
IMailHelper mailHelper)
{
this.userHelper = userHelper;
this.countryRepository = countryRepository;
this.mailHelper = mailHelper;
}
[HttpPost]
public async Task<IActionResult> PostUser([FromBody] NewUserRequest request)
{
303
if (!ModelState.IsValid)
{
return this.BadRequest(new Response
{
IsSuccess = false,
Message = "Bad request"
});
}
304
LastName = request.LastName,
Email = request.Email,
UserName = request.Email,
Address = request.Address,
PhoneNumber = request.Phone,
CityId = request.CityId,
City = city
};
305
}
306
Message = ex.Message,
};
}
}
if (string.IsNullOrEmpty(this.LastName))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter the last name.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.Email))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter an email.",
307
"Accept");
return;
}
if (!RegexHelper.IsValidEmail(this.Email))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter a valid email.",
"Accept");
return;
}
if (this.Country == null)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must select a country.",
"Accept");
return;
}
if (this.City == null)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must select a city.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.Address))
308
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter an address.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.Phone))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter a phone number.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.Password))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter a password.",
"Accept");
return;
}
if (this.Password.Length < 6)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You password must be at mimimun 6 characters.",
"Accept");
309
return;
}
if (string.IsNullOrEmpty(this.Confirm))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter a password confirm.",
"Accept");
return;
}
if (!this.Password.Equals(this.Confirm))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"The password and the confirm do not match.",
"Accept");
return;
}
this.IsRunning = true;
this.IsEnabled = false;
310
Phone = this.Phone
};
this.IsRunning = false;
this.IsEnabled = true;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
response.Message,
"Accept");
return;
}
await Application.Current.MainPage.DisplayAlert(
"Ok",
response.Message,
"Accept");
await Application.Current.MainPage.Navigation.PopAsync();
}
311
Recover Password From App in Xamarin Forms
1. Add the RememberPasswordPage:
using System.Windows.Input;
using Common.Helpers;
using Common.Services;
using GalaSoft.MvvmLight.Command;
using Xamarin.Forms;
313
get => this.isEnabled;
set => this.SetValue(ref this.isEnabled, value);
}
public RememberPasswordViewModel()
{
this.apiService = new ApiService();
this.IsEnabled = true;
}
if (!RegexHelper.IsValidEmail(this.Email))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter a valid email.",
"Accept");
return;
314
}
}
}
315
using System.ComponentModel.DataAnnotations;
[HttpPost]
[Route("RecoverPassword")]
public async Task<IActionResult> RecoverPassword([FromBody] RecoverPasswordRequest request)
{
if (!ModelState.IsValid)
{
return this.BadRequest(new Response
{
IsSuccess = false,
Message = "Bad request"
});
}
316
var myToken = await this.userHelper.GeneratePasswordResetTokenAsync(user);
var link = this.Url.Action("ResetPassword", "Account", new { token = myToken }, protocol: HttpContext.Request.Scheme);
this.mailHelper.SendMail(request.Email, "Password Reset", $"<h1>Recover Password</h1>" +
$"To reset the password click in this link:</br></br>" +
$"<a href = \"{link}\">Reset Password</a>");
9. Publish on Azure.
317
var url = $"{servicePrefix}{controller}";
var response = await client.PostAsync(url, content);
var answer = await response.Content.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<Response>(answer);
return obj;
}
catch (Exception ex)
{
return new Response
{
IsSuccess = false,
Message = ex.Message,
};
}
}
if (!RegexHelper.IsValidEmail(this.Email))
{
318
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter a valid email.",
"Accept");
return;
}
this.IsRunning = true;
this.IsEnabled = false;
this.IsRunning = false;
this.IsEnabled = true;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
response.Message,
"Accept");
return;
319
}
await Application.Current.MainPage.DisplayAlert(
"Ok",
response.Message,
"Accept");
await Application.Current.MainPage.Navigation.PopAsync();
}
[HttpPost]
[Route("GetUserByEmail")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<IActionResult> GetUserByEmail([FromBody] RecoverPasswordRequest request)
{
if (!ModelState.IsValid)
{
return this.BadRequest(new Response
{
IsSuccess = false,
Message = "Bad request"
});
}
320
{
return this.BadRequest(new Response
{
IsSuccess = false,
Message = "User don't exists."
});
}
return Ok(user);
}
2. Publish on Azure.
321
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(tokenType, accessToken);
var url = $"{servicePrefix}{controller}";
var response = await client.PostAsync(url, content);
var answer = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
return new Response
{
IsSuccess = false,
Message = answer,
};
}
322
[JsonProperty("cityId")]
public int CityId { get; set; }
[JsonProperty("address")]
public string Address { get; set; }
323
token.Token);
Settings.IsRemember = this.IsRemember;
Settings.UserEmail = this.Email;
Settings.UserPassword = this.Password;
Settings.Token = JsonConvert.SerializeObject(token);
Settings.User = JsonConvert.SerializeObject(user);
public App()
{
InitializeComponent();
if (Settings.IsRemember)
{
var token = JsonConvert.DeserializeObject<TokenResponse>(Settings.Token);
var user = JsonConvert.DeserializeObject<User>(Settings.User);
324
var mainViewModel = MainViewModel.GetInstance();
mainViewModel.Token = token;
mainViewModel.User = user;
mainViewModel.UserEmail = Settings.UserEmail;
mainViewModel.UserPassword = Settings.UserPassword;
mainViewModel.Products = new ProductsViewModel();
this.MainPage = new MasterPage();
return;
}
}
325
Icon = "ic_person",
PageName = "ProfilePage",
Title = "Modify User"
},
new Menu
{
Icon = "ic_phonelink_setup",
PageName = "SetupPage",
Title = "Setup"
},
new Menu
{
Icon = "ic_exit_to_app",
PageName = "LoginPage",
Title = "Close session"
}
};
<Image
HeightRequest="150"
Source="shop.png">
</Image>
326
<Label
FontSize="Large"
Text="{Binding User.FullName, StringFormat='Welcome: {0}'}"
TextColor="White">
</Label>
<ListView
327
</Label>
<Entry
Grid.Column="1"
Grid.Row="0"
Placeholder="Enter your first name..."
Text="{Binding User.FirstName}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="1"
Text="Last name"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="1"
Placeholder="Enter your last name..."
Text="{Binding User.LastName}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="2"
Text="Country"
VerticalOptions="Center">
</Label>
<Picker
Grid.Column="1"
Grid.Row="2"
ItemDisplayBinding="{Binding Name}"
ItemsSource="{Binding Countries}"
SelectedItem="{Binding Country}"
Title="Select a country...">
328
</Picker>
<Label
Grid.Column="0"
Grid.Row="3"
Text="City"
VerticalOptions="Center">
</Label>
<Picker
Grid.Column="1"
Grid.Row="3"
ItemDisplayBinding="{Binding Name}"
ItemsSource="{Binding Cities}"
SelectedItem="{Binding City}"
Title="Select a city...">
</Picker>
<Label
Grid.Column="0"
Grid.Row="4"
Text="Address"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="4"
Placeholder="Enter your address..."
Text="{Binding User.Address}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="5"
Text="Phone"
VerticalOptions="Center">
329
</Label>
<Entry
Grid.Column="1"
Grid.Row="5"
Keyboard="Telephone"
Placeholder="Enter your phone number..."
Text="{Binding User.PhoneNumber}">
</Entry>
</Grid>
<ActivityIndicator
IsRunning="{Binding IsRunning}"
VerticalOptions="CenterAndExpand">
</ActivityIndicator>
<StackLayout
Orientation="Horizontal">
<Button
BackgroundColor="Navy"
BorderRadius="23"
Command="{Binding SaveCommand}"
HeightRequest="46"
HorizontalOptions="FillAndExpand"
IsEnabled="{Binding IsEnabled}"
Text="Save"
TextColor="White">
</Button>
<Button
BackgroundColor="Purple"
BorderRadius="23"
Command="{Binding ModifyPasswordCommand}"
HeightRequest="46"
HorizontalOptions="FillAndExpand"
IsEnabled="{Binding IsEnabled}"
330
Text="Modify Password"
TextColor="White">
</Button>
</StackLayout>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Common.Models;
using Common.Services;
using Xamarin.Forms;
331
get => this.country;
set
{
this.SetValue(ref this.country, value);
this.Cities = new ObservableCollection<City>(this.Country.Cities.OrderBy(c => c.Name));
}
}
332
public bool IsRunning
{
get => this.isRunning;
set => this.SetValue(ref this.isRunning, value);
}
public ProfileViewModel()
{
this.apiService = new ApiService();
this.User = MainViewModel.GetInstance().User;
this.IsEnabled = true;
this.LoadCountries();
}
this.IsRunning = false;
333
this.IsEnabled = true;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
response.Message,
"Accept");
return;
}
this.myCountries = (List<Country>)response.Result;
this.Countries = new ObservableCollection<Country>(myCountries);
this.SetCountryAndCity();
}
334
public ProfileViewModel Profile { get; set; }
switch (this.PageName)
{
case "AboutPage":
await App.Navigator.PushAsync(new AboutPage());
break;
case "SetupPage":
await App.Navigator.PushAsync(new SetupPage());
break;
case "ProfilePage":
mainViewModel.Profile = new ProfileViewModel();
await App.Navigator.PushAsync(new ProfilePage());
break;
default:
Settings.User = string.Empty;
Settings.IsRemember = false;
Settings.Token = string.Empty;
Settings.UserEmail = string.Empty;
Settings.UserPassword = string.Empty;
335
}
}
[HttpPut]
public async Task<IActionResult> PutUser([FromBody] User user)
{
if (!ModelState.IsValid)
{
return this.BadRequest(ModelState);
}
userEntity.FirstName = user.FirstName;
userEntity.LastName = user.LastName;
userEntity.CityId = user.CityId;
userEntity.Address = user.Address;
userEntity.PhoneNumber = user.PhoneNumber;
336
var respose = await this.userHelper.UpdateUserAsync(userEntity);
if (!respose.Succeeded)
{
return this.BadRequest(respose.Errors.FirstOrDefault().Description);
}
337
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(tokenType, accessToken);
var url = $"{servicePrefix}{controller}";
var response = await client.PutAsync(url, content);
var answer = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
return new Response
{
IsSuccess = false,
Message = answer,
};
}
338
public ICommand SaveCommand => new RelayCommand(this.Save);
…
private async void Save()
{
if (string.IsNullOrEmpty(this.User.FirstName))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter the first name.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.User.LastName))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter the last name.",
"Accept");
return;
}
if (this.Country == null)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must select a country.",
"Accept");
return;
}
if (this.City == null)
339
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must select a city.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.User.Address))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter an address.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.User.PhoneNumber))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter a phone number.",
"Accept");
return;
}
this.IsRunning = true;
this.IsEnabled = false;
340
"/api",
"/Account",
this.User,
"bearer",
MainViewModel.GetInstance().Token.Token);
this.IsRunning = false;
this.IsEnabled = true;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
response.Message,
"Accept");
return;
}
MainViewModel.GetInstance().User = this.User;
Settings.User = JsonConvert.SerializeObject(this.User);
await Application.Current.MainPage.DisplayAlert(
"Ok",
"User updated!",
"Accept");
await App.Navigator.PopAsync();
}
341
private static MainViewModel instance;
private User user;
using System.ComponentModel.DataAnnotations;
[Required]
public string NewPassword { get; set; }
[Required]
public string Email { get; set; }
}
342
[HttpPost]
[Route("ChangePassword")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
{
if (!ModelState.IsValid)
{
return this.BadRequest(new Response
{
IsSuccess = false,
Message = "Bad request"
});
}
343
}
3. Publish on Azure.
344
var response = await client.PostAsync(url, content);
var answer = await response.Content.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<Response>(answer);
return obj;
}
catch (Exception ex)
{
return new Response
{
IsSuccess = false,
Message = ex.Message,
};
}
}
345
</Grid.ColumnDefinitions>
<Label
Grid.Column="0"
Grid.Row="0"
Text="Current password"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="0"
IsPassword="True"
Placeholder="Enter your current password..."
Text="{Binding CurrentPassword}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="1"
Text="New password"
VerticalOptions="Center">
</Label>
<Entry
Grid.Column="1"
Grid.Row="1"
IsPassword="True"
Placeholder="Enter the new password..."
Text="{Binding NewPassword}">
</Entry>
<Label
Grid.Column="0"
Grid.Row="2"
Text="Confirm new password"
VerticalOptions="Center">
346
</Label>
<Entry
Grid.Column="1"
Grid.Row="2"
IsPassword="True"
Placeholder="Renter the new password..."
Text="{Binding PasswordConfirm}">
</Entry>
</Grid>
<ActivityIndicator
IsRunning="{Binding IsRunning}"
VerticalOptions="CenterAndExpand">
</ActivityIndicator>
<Button
BackgroundColor="Navy"
BorderRadius="23"
Command="{Binding ChangePasswordCommand}"
HeightRequest="46"
HorizontalOptions="FillAndExpand"
IsEnabled="{Binding IsEnabled}"
Text="Change Password"
TextColor="White">
</Button>
</StackLayout>
</ScrollView>
</ContentPage.Content>
</ContentPage>
using System.Windows.Input;
using Common.Models;
347
using Common.Services;
using GalaSoft.MvvmLight.Command;
using Shop.Common.Helpers;
using Xamarin.Forms;
public ChangePasswordViewModel()
348
{
this.apiService = new ApiService();
this.IsEnabled = true;
}
if (!MainViewModel.GetInstance().UserPassword.Equals(this.CurrentPassword))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"The current password is incorrect.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.NewPassword))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter the new password.",
"Accept");
return;
349
}
if (this.NewPassword.Length < 6)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"The password must have at least 6 characters length.",
"Accept");
return;
}
if (string.IsNullOrEmpty(this.PasswordConfirm))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"You must enter the password confirm.",
"Accept");
return;
}
if (!this.NewPassword.Equals(this.PasswordConfirm))
{
await Application.Current.MainPage.DisplayAlert(
"Error",
"The password and confirm does not match.",
"Accept");
return;
}
this.IsRunning = true;
this.IsEnabled = false;
350
var request = new ChangePasswordRequest
{
Email = MainViewModel.GetInstance().UserEmail,
NewPassword = this.NewPassword,
OldPassword = this.CurrentPassword
};
this.IsRunning = false;
this.IsEnabled = true;
if (!response.IsSuccess)
{
await Application.Current.MainPage.DisplayAlert(
"Error",
response.Message,
"Accept");
return;
}
MainViewModel.GetInstance().UserPassword = this.NewPassword;
Settings.UserPassword = this.NewPassword;
await Application.Current.MainPage.DisplayAlert(
351
"Ok",
response.Message,
"Accept");
await App.Navigator.PopAsync();
}
}
9. Test it.
</style>
352
<style name="Theme.Splash" parent="android:Theme">
<item name="android:windowBackground">@drawable/splash</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>
using Android.App;
using Android.OS;
[Activity(
Theme = "@style/Theme.Splash",
MainLauncher = true,
NoHistory = true)]
public class SplashActivity : Activity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
System.Threading.Thread.Sleep(1800);
this.StartActivity(typeof(MainActivity));
}
}
[Activity(
Label = "Shop",
Icon = "@mipmap/icon",
Theme = "@style/MainTheme",
MainLauncher = false,
353
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
5. Test it.
6. Now add the icon launcher. Go to https://romannurik.github.io/AndroidAssetStudio/ and personalizate your own icon launcher.
And add the image to Android and iOS projects.
8. Test it.
2. Ensure the you App have an Icon and this other properties:
354
3. Generate the APK for Publish:
355
4. Then click on “Distribute”:
356
5. Then in a “Ah Hoc” button:
357
6. The first time, you need to create the “Sign” for the app, you shouldn’t forget the key :
358
7. Set the project on Release mode:
359
360
8. Click on “Save As”, select a location and then put the password provide before:
361
9. You need the generated APK for the following steps. Enter to: https://developer.android.com/distribute/console?hl=es and
click on “CREATE APPLICATION”:
362
11. Take application screenshots and put in this page:
12. You need the application icon in 512 x 512 pixels and image in 1024 x 500. Add in this page:
363
13. Fill this sections:, save as a draft and answer the content rating questionnaire:
364
14. Go to App Releases and create a new one in production.
365
366
15. Upload your APK:
367
17. Go to content rating:
368
18. Answer all the questions and click on “SAVE QUESTIONNAIRE”:
369
20. And then “APPLY RATING”:
21. Now go pricing & distribution. Fill the required fields in this part:
370
22. Make the available countries:
371
23. Save and click on “Ready to publish”. Click on “EDIT RELEASE”:
26. Then wait some time, about 40 minutes to be able to download from Google Play.
372
27. Wait until appears this message:
28. Now you can download the app and test it!
373
android:layout_height="wrap_content">
<TextView
android:text="Email"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/emailText" />
<TextView
android:text="Password"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/passwordText" />
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:id="@+id/activityIndicatorProgressBar"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
<Button
374
android:text="Login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/loginButton" />
</LinearLayout>
</RelativeLayout>
19. Add the folder Helpers and inside it, add the DiaglogService:
using global::Android.App;
using global::Android.Content;
20. Add the folder Activities and inside it, add the LoginActivity:
using System;
using global::Android.App;
using global::Android.Content;
using global::Android.OS;
using global::Android.Support.V7.App;
375
using global::Android.Views;
using global::Android.Widget;
using Newtonsoft.Json;
using Shop.Common.Models;
using Shop.Common.Services;
using Shop.UIClassic.Android.Helpers;
376
private void HandleEvents()
{
this.loginButton.Click += this.LoginButton_Click;
}
if (string.IsNullOrEmpty(this.passwordText.Text))
{
DiaglogService.ShowMessage(this, "Error", "You must enter a password.", "Accept");
return;
}
this.activityIndicatorProgressBar.Visibility = ViewStates.Visible;
377
request);
this.activityIndicatorProgressBar.Visibility = ViewStates.Invisible;
if (!response.IsSuccess)
{
DiaglogService.ShowMessage(this, "Error", "User or password incorrect.", "Accept");
return;
}
<resources>
<string name="app_name">Shop</string>
<string name="action_settings">Settings</string>
</resources>
378
24. In layout add the ProductsPage:
379
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp">
<TextView
android:id="@+id/nameTextView"
android:layout_width="match_parent"
android:textSize="24dp"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:layout_alignParentLeft="true"
android:textStyle="bold"
android:gravity="left" />
<TextView
android:id="@+id/priceTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:gravity="left"
android:textSize="18dp"
android:textColor="@android:color/black" />
</LinearLayout>
</LinearLayout>
using System.Net;
using global::Android.Graphics;
380
if (string.IsNullOrEmpty(url))
{
return null;
}
return imageBitmap;
}
}
27. Create the folder Adapters and inside it add the class ProductsListAdapter:
using System.Collections.Generic;
using Common.Models;
using global::Android.App;
using global::Android.Views;
using global::Android.Widget;
using Helpers;
381
private readonly Activity context;
if (convertView == null)
{
convertView = context.LayoutInflater.Inflate(Resource.Layout.ProductRow, null);
}
convertView.FindViewById<TextView>(Resource.Id.nameTextView).Text = item.Name;
convertView.FindViewById<TextView>(Resource.Id.priceTextView).Text = $"{item.Price:C2}";
convertView.FindViewById<ImageView>(Resource.Id.productImageView).SetImageBitmap(imageBitmap);
382
return convertView;
}
}
using System.Collections.Generic;
using Adapters;
using Common.Models;
using Common.Services;
using global::Android.App;
using global::Android.Content;
using global::Android.OS;
using global::Android.Support.V7.App;
using global::Android.Widget;
using Helpers;
using Newtonsoft.Json;
this.productsListView = FindViewById<ListView>(Resource.Id.productsListView);
383
this.email = Intent.Extras.GetString("email");
var tokenString = Intent.Extras.GetString("token");
this.token = JsonConvert.DeserializeObject<TokenResponse>(tokenString);
if (!response.IsSuccess)
{
DiaglogService.ShowMessage(this, "Error", response.Message, "Accept");
return;
}
384
this.activityIndicatorProgressBar.Visibility = ViewStates.Gone;
385
5. Modify the Main.storyboard.
386
6. Modify the ViewController:
using System;
using UIKit;
387
public override void ViewDidLoad()
{
base.ViewDidLoad();
}
if (string.IsNullOrEmpty(this.PasswordText.Text))
{
var alert = UIAlertController.Create("Error", "You must enter a password.", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("Accept", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
return;
}
388
}
}
7. Test it.
this.DoLogin();
}
if (!response.IsSuccess)
{
this.ActivityIndicator.StopAnimating();
389
var alert = UIAlertController.Create("Error", "User or password incorrect.", UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("Accept", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
return;
}
390
10. Test the button Test.
if (!response2.IsSuccess)
{
var alert = UIAlertController.Create("Error", response.Message, UIAlertControllerStyle.Alert);
alert.AddAction(UIAlertAction.Create("Accept", UIAlertActionStyle.Default, null));
this.PresentViewController(alert, true, null);
return;
}
Settings.UserEmail = this.EmailText.Text;
Settings.Token = JsonConvert.SerializeObject(token);
Settings.Products = JsonConvert.SerializeObject(products);
391
this.ActivityIndicator.StopAnimating();
14. Add the folder DataSources and inside it, add the class ProductsDataSource:
using System;
using System.Collections.Generic;
using Common.Models;
using Foundation;
using UIKit;
392
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Default, cellIdentifier);
}
return cell;
}
393
16. Modify the ProductsViewController:
using System;
using System.Collections.Generic;
using Common.Helpers;
using Common.Models;
using DataSources;
using Newtonsoft.Json;
using UIKit;
394
}
18. Add the folder Cells and inside it add the class ProductCell:
using System.Drawing;
using Foundation;
using UIKit;
395
{
TextAlignment = UITextAlignment.Right
};
this.ContentView.Add(this.nameLabel);
this.ContentView.Add(this.priceLabel);
this.ContentView.Add(this.imageView);
}
396
if (cell == null)
{
cell = new ProductCell(cellIdentifier);
}
return cell;
}
397
Starting With MVVM Cross, Test Concept
Way One
Xamarin Forms
Way Two
Xamarin Classic
398
2. Delete Class1.
5. Congratulations you have ready the initial foundation for the solution.
399
Forms Traditional Project - Way ONE
1. Add a traditional Xamarin Forms project call: FourWays.FormsTraditional and move all the projects to the correct folder:
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
400
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
backingField = value;
this.OnPropertyChanged(propertyName);
}
}
using FourWays.Core.Services;
401
this.Recalculate();
}
}
public MainViewModel()
{
this.calculationService = new CalculationService();
this.Amount = 100;
this.Generosity = 10;
}
402
this.Tip = this.calculationService.TipAmount(this.Amount, this.Generosity);
}
}
using ViewModels;
public InstanceLocator()
{
this.Main = new MainViewModel();
}
}
403
Text="Amount:">
</Label>
<Entry
Keyboard="Numeric"
BackgroundColor="White"
TextColor="Black"
Text="{Binding Amount, Mode=TwoWay}">
</Entry>
<Label
TextColor="White"
Text="Generosity:">
</Label>
<Slider
Minimum="0"
Maximum="100"
Value="{Binding Generosity, Mode=TwoWay}">
</Slider>
<Label
TextColor="White"
Text="Tip:">
</Label>
<Label
TextColor="Yellow"
FontAttributes="Bold"
FontSize="Large"
HorizontalTextAlignment="Center"
Text="{Binding Tip, Mode=TwoWay}">
</Label>
</StackLayout>
</ContentPage.Content>
</ContentPage>
404
8. Modify the App.xaml:
using Views;
using Xamarin.Forms;
405
Xamarin Android Classic - Way TWO
1. Add a Xamarin Android project call: FourWays.Classic.Droid, using blank template.
406
<EditText
android:text="100"
android:id="@+id/amountEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number|numberDecimal"
android:textSize="24dp"
android:gravity="right" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="24dp"
android:text="Generosity" />
<SeekBar
android:id="@+id/generositySeekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:min="0"
android:progress="10" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="30dp"
android:background="@android:color/darker_gray" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24dp"
android:text="Tip to leave" />
<TextView
407
android:id="@+id/tipTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/holo_blue_dark"
android:textSize="24dp"
android:gravity="center" />
</LinearLayout>
<resources>
<string name="app_name">Tip Calc</string>
<string name="action_settings">Settings</string>
</resources>
using System;
using Android.App;
using Android.OS;
using Android.Support.V7.App;
using Android.Widget;
using Core.Services;
[Activity(
Label = "@string/app_name",
Theme = "@style/AppTheme",
MainLauncher = true)]
public class MainActivity : AppCompatActivity
{
private EditText amountEditText;
private SeekBar generositySeekBar;
408
private TextView tipTextView;
private ICalculationService calculationService;
this.SetContentView(Resource.Layout.activity_main);
this.calculationService = new CalculationService();
this.FindViews();
this.SetupEvents();
}
409
var generosity = (double)this.generositySeekBar.Progress;
this.tipTextView.Text = $"{this.calculationService.TipAmount(amount, generosity):C2}";
}
6. Test it.
410
2. Add a reference to FourWays.Core.
411
5. Modify the ViewController.
using System;
using FourWays.Core.Services;
using UIKit;
this.AmountText.EditingChanged += AmountText_EditingChanged;
this.GenerositySlider.ValueChanged += GenerositySlider_ValueChanged;
}
413
}
}
6. Test it.
2. Add the folder ViewModels and the class TipViewModel inside it.
using System.Threading.Tasks;
using MvvmCross.ViewModels;
using Services;
#region Properties
public decimal SubTotal
{
get
{
return this.subTotal;
}
414
set
{
this.subTotal = value;
this.RaisePropertyChanged(() => this.SubTotal);
this.Recalculate();
}
}
415
}
}
#endregion
#region Constructors
public TipViewModel(ICalculationService calculationService)
{
this.calculationService = calculationService;
}
#endregion
#region Methods
public override async Task Initialize()
{
await base.Initialize();
this.SubTotal = 100;
this.Generosity = 10;
this.Recalculate();
}
using MvvmCross.IoC;
using MvvmCross.ViewModels;
416
using ViewModels;
this.RegisterAppStart<TipViewModel>();
}
}
4. Congratulations you have ready the complete foundation for the solution.
417
2. Add the reference to Core project and add the NuGet MvvmCross.
5. Into Resources folder, add the folder drawable and inside it add the files Icon.png and splash.png (you can get it for my
repository https://github.com/Zulu55/Shop select a branch different to master).
418
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Loading...." />
</LinearLayout>
419
android:text="Generosity" />
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
local:MvxBind="Progress Generosity" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_margin="30dp"
android:background="@android:color/darker_gray" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="24dp"
android:text="Tip to leave" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/holo_blue_dark"
android:textSize="24dp"
android:gravity="center"
local:MvxBind="Text Tip" />
</LinearLayout>
<resources>
<string name="app_name">Tip Calc</string>
<string name="action_settings">Settings</string>
</resources>
420
9. In values folder add the SplashStyle.xml file.
10. Add the folder Views and inside it add the class SplashView.
using Android.App;
using Android.Content.PM;
using Core;
using MvvmCross.Platforms.Android.Core;
using MvvmCross.Platforms.Android.Views;
[Activity(
Label = "@string/app_name",
MainLauncher = true,
Icon = "@drawable/icon",
Theme = "@style/Theme.Splash",
NoHistory = true,
ScreenOrientation = ScreenOrientation.Portrait)]
public class SplashView : MvxSplashScreenActivity<MvxAndroidSetup<App>, App>
{
public SplashView() : base(Resource.Layout.SplashPage)
{
}
}
421
11. In the folder Views add TipView.
using Android.App;
using Android.OS;
using Core.ViewModels;
using MvvmCross.Platforms.Android.Views;
[Activity(Label = "@string/app_name")]
public class TipView : MvxActivity<TipViewModel>
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
this.SetContentView(Resource.Layout.TipPage);
}
}
422
2. Add the reference to Core project and add the NuGet MvvmCross.
using Core;
using Foundation;
using MvvmCross.Platforms.Ios.Core;
[Register("AppDelegate")]
public class AppDelegate : MvxApplicationDelegate<MvxIosSetup<App>, App>
{
}
423
5. Modify the view Main.storyboard similar to this:
424
6. Modify the class ViewController:
using Core.ViewModels;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Platforms.Ios.Presenters.Attributes;
using MvvmCross.Platforms.Ios.Views;
[MvxRootPresentation(WrapInNavigationController = true)]
public partial class ViewController : MvxViewController<TipViewModel>
{
public override void ViewDidLoad()
425
{
base.ViewDidLoad();
426
2. Add the reference to FourWays.Core in FourWays.FormCross, FourWays.FormCross.Android and
FourWays.FormCross.iOS.
427
<?xml version="1.0" encoding="utf-8" ?>
<Application xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="FourWays.FormsCross.FormsApp">
<Application.Resources>
</Application.Resources>
</Application>
using Xamarin.Forms;
8. Add the folder Views and inside it, create the TipView.xaml:
428
<ContentPage.Content>
<StackLayout Margin="10">
<Label Text="Subtotal" />
<Entry
x:Name="SubTotalEntry"
Keyboard="Numeric"
mvx:Bi.nd="Text SubTotal, Mode=TwoWay">
</Entry>
<Label
Text="Generosity">
</Label>
<Slider
x:Name="GenerositySlider"
Maximum="100"
mvx:Bi.nd="Value Generosity, Mode=TwoWay">
</Slider>
<Label
Text="Tip to leave">
</Label>
<Label
x:Name="TipLabel"
mvx:Bi.nd="Text Tip">
</Label>
</StackLayout>
</ContentPage.Content>
</views:MvxContentPage>
using Core.ViewModels;
using MvvmCross.Forms.Views;
429
public partial class TipView : MvxContentPage<TipViewModel>
{
public TipView()
{
InitializeComponent();
}
}
using Android.App;
using Android.Content.PM;
using Android.OS;
using Core;
using MvvmCross.Forms.Platforms.Android.Core;
using MvvmCross.Forms.Platforms.Android.Views;
[Activity(
Label = "Tip Calc",
Icon = "@mipmap/icon",
Theme = "@style/MainTheme",
MainLauncher = true,
ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,
LaunchMode = LaunchMode.SingleTask)]
public class MainActivity : MvxFormsAppCompatActivity<MvxFormsAndroidSetup<App, FormsApp>, App, FormsApp>
{
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(bundle);
}
430
}
using Core;
using Foundation;
using MvvmCross.Forms.Platforms.Ios.Core;
using UIKit;
[Register(nameof(AppDelegate))]
public partial class AppDelegate : MvxFormsApplicationDelegate<MvxFormsIosSetup<App, FormsApp>, App, FormsApp>
{
public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions)
{
return base.FinishedLaunching(uiApplication, launchOptions);
}
}
431
MVVM Cross Value Converters
Core Project
1. Add the folder Converters and inside it, create the class: DecimalToStringValueConverter, it’s very important that the class
name ends with ValueConverter.
using MvvmCross.Converters;
using System;
using System.Globalization;
Android Project
1. Change the Page to call the converter:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/holo_blue_dark"
android:textSize="24dp"
android:gravity="center"
local:MvxBind="Text Tip,Converter=DecimalToString" />
432
2. Test it.
iOS Project
1. Change the controller to call the converter:
2. Test it.
433
3. Extract the interface to ApiService:
using System.Threading.Tasks;
using Models;
Task<Response> GetListAsync<T>(
string urlBase,
string servicePrefix,
string controller);
Task<Response> GetListAsync<T>(
string urlBase,
string servicePrefix,
string controller,
string tokenType,
string accessToken);
Task<Response> GetTokenAsync(
string urlBase,
string servicePrefix,
string controller,
434
TokenRequest request);
Task<Response> PostAsync<T>(
string urlBase,
string servicePrefix,
string controller,
T model,
string tokenType,
string accessToken);
Task<Response> PutAsync<T>(
string urlBase,
string servicePrefix,
string controller,
int id,
T model,
string tokenType,
string accessToken);
}
4. Add the folder Interfaces and inside it add the interface IDialogService:
5. Add the folder ViewModels and inside it add the class LoginViewModel:
using System.Windows.Input;
using Interfaces;
using Models;
435
using MvvmCross.Commands;
using MvvmCross.ViewModels;
using Services;
436
{
get
{
this.loginCommand = this.loginCommand ?? new MvxCommand(this.DoLoginCommand);
return this.loginCommand;
}
}
public LoginViewModel(
IApiService apiService,
IDialogService dialogService)
{
this.apiService = apiService;
this.dialogService = dialogService;
this.Email = "jzuluaga55@gmail.com";
this.Password = "123456";
this.IsLoading = false;
}
if (string.IsNullOrEmpty(this.Password))
{
this.dialogService.Alert("Error", "You must enter a password.", "Accept");
return;
437
}
this.IsLoading = true;
if (!response.IsSuccess)
{
this.IsLoading = false;
this.dialogService.Alert("Error", "User or password incorrect.", "Accept");
return;
}
this.IsLoading = false;
this.dialogService.Alert("Ok", "Fuck yeah!", "Accept");
}
}
using MvvmCross.IoC;
using MvvmCross.ViewModels;
438
using ViewModels;
this.RegisterAppStart<LoginViewModel>();
}
}
439
4. Delete the MainActivity activity and the activity_main layout.
5. Into Resources folder, add the folder drawable and inside it add the files Icon.png and splash.png (you can get it from the
repository: https://github.com/Zulu55/Shop, select the branch Group 3)
440
android:paddingRight="10dp"
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:text="Email"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Email" />
<TextView
android:text="Password"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Password" />
<ProgressBar
android:layout_height="wrap_content"
441
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
<Button
android:text="Login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Click LoginCommand" />
</LinearLayout>
</RelativeLayout>
<resources>
<string name="app_name">Shop</string>
<string name="action_settings">Settings</string>
</resources>
8. Add the folder Views and inside it add the class SplashView.
using global::Android.App;
442
using global::Android.Content.PM;
using MvvmCross.Platforms.Android.Views;
[Activity(
Label = "@string/app_name",
MainLauncher = true,
Icon = "@drawable/icon",
Theme = "@style/Theme.Splash",
NoHistory = true,
ScreenOrientation = ScreenOrientation.Portrait)]
public class SplashView : MvxSplashScreenActivity
{
public SplashView() : base(Resource.Layout.SplashPage)
{
}
}
using Common.ViewModels;
using global::Android.App;
using global::Android.OS;
using MvvmCross.Platforms.Android.Views;
[Activity(Label = "@string/app_name")]
public class LoginView : MvxActivity<LoginViewModel>
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
this.SetContentView(Resource.Layout.LoginPage);
}
443
}
10. Add the folder Services and inside it add the class DialogService:
using Common.Interfaces;
using global::Android.App;
using MvvmCross;
using MvvmCross.Platforms.Android;
using Common;
using Common.Interfaces;
using MvvmCross;
using MvvmCross.Platforms.Android.Core;
using Services;
using System.Collections.Generic;
444
using System.Linq;
using System.Reflection;
base.InitializeFirstChance();
}
445
2. Add the reference to Common project.
using Foundation;
using MvvmCross.Platforms.Ios.Core;
[Register("AppDelegate")]
public class AppDelegate : MvxApplicationDelegate
{
}
446
5. Add the folder Views and inside it add the view HomeView.xib, HomeView.cs and HomeView.designer.cs. And fix the
namespaces.
447
7. Modify the class HomeView:
using Common.ViewModels;
using MvvmCross.Binding.BindingContext;
using MvvmCross.Platforms.Ios.Presenters.Attributes;
448
using MvvmCross.Platforms.Ios.Views;
[MvxRootPresentation(WrapInNavigationController = true)]
public partial class HomeView : MvxViewController<LoginViewModel>
{
public HomeView() : base("HomeView", null)
{
}
using MvvmCross.Platforms.Ios.Core;
using MvvmCross.ViewModels;
449
9. You’re ready to test the project on iOS!
using System.Collections.Generic;
using Helpers;
using Interfaces;
using Models;
using MvvmCross.ViewModels;
using Newtonsoft.Json;
using Services;
public ProductsViewModel(
IApiService apiService,
IDialogService dialogService)
{
this.apiService = apiService;
450
this.dialogService = dialogService;
this.LoadProducts();
}
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
this.Products = (List<Product>)response.Result;
}
}
using System;
using System.Globalization;
using MvvmCross.Converters;
451
protected override string Convert(decimal value, Type targetType, object parameter, CultureInfo culture)
{
return $"{value:C2}";
}
}
using System.Windows.Input;
using Interfaces;
using Models;
using MvvmCross.Commands;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;
using Newtonsoft.Json;
using Services;
using Shop.Common.Helpers;
452
}
public LoginViewModel(
IApiService apiService,
IDialogService dialogService,
IMvxNavigationService navigationService)
{
this.apiService = apiService;
this.dialogService = dialogService;
this.navigationService = navigationService;
453
this.Email = "jzuluaga55@gmail.com";
this.Password = "123456";
this.IsLoading = false;
}
if (string.IsNullOrEmpty(this.Email))
{
this.dialogService.Alert("Error", "You must enter a password.", "Accept");
return;
}
this.IsLoading = true;
454
if (!response.IsSuccess)
{
this.IsLoading = false;
this.dialogService.Alert("Error", "User or password incorrect.", "Accept");
return;
}
455
android:layout_height="fill_parent">
<ffimageloading.cross.MvxCachedImageView
android:layout_width="75dp"
android:layout_height="75dp"
android:layout_margin="10dp"
local:MvxBind="ImagePath ImageFullPath" />
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="30dp"
local:MvxBind="Text Name" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textSize="20dp"
local:MvxBind="Text Price,Converter=DecimalToString" />
</LinearLayout>
</LinearLayout>
456
<MvxListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="ItemsSource Products"
local:MvxItemTemplate="@layout/productrow"/>
</LinearLayout>
using Common.ViewModels;
using global::Android.App;
using global::Android.OS;
using MvvmCross.Platforms.Android.Views;
[Activity(Label = "@string/app_name")]
public class ProductsView : MvxActivity<ProductsViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.SetContentView(Resource.Layout.ProductsPage);
}
}
5. Test it in Android.
using System.Windows.Input;
using Helpers;
457
using Interfaces;
using Models;
using MvvmCross.Commands;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;
using Newtonsoft.Json;
using Services;
458
get => this.price;
set => this.SetProperty(ref this.price, value);
}
public AddProductViewModel(
IApiService apiService,
IDialogService dialogService,
IMvxNavigationService navigationService)
{
this.apiService = apiService;
this.dialogService = dialogService;
this.navigationService = navigationService;
}
if (string.IsNullOrEmpty(this.Price))
459
{
this.dialogService.Alert("Error", "You must enter a product price.", "Accept");
return;
}
this.IsLoading = true;
460
this.IsLoading = false;
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
461
Android Third Part (Add Product)
1. Add the noImage.png, shop.png and ic_add.png in folder drawable on Shop.UICross.Android.
462
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Email" />
<TextView
android:text="Password"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Password" />
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_gravity="center_vertical"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
</LinearLayout>
<Button
android:text="Login"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
463
android:layout_height="wrap_content"
local:MvxBind="Click LoginCommand" />
</RelativeLayout>
464
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:local="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_gravity="center"
android:src="@drawable/noImage"
android:layout_width="300dp"
android:layout_height="200dp" />
<TextView
android:text="Name"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Name" />
<TextView
465
android:text="Price"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="numberDecimal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Price" />
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
<Button
android:text="Add Product"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Click AddProductCommand" />
</LinearLayout>
</RelativeLayout>
<resources>
<string name="app_name">Shop</string>
<string name="add_product">Add Product</string>
<string name="action_settings">Settings</string>
</resources>
466
5. Add the AddProductView in Shop.UICross.Android.Views:
using global::Android.App;
using global::Android.OS;
using MvvmCross.Platforms.Android.Views;
using Shop.Common.ViewModels;
[Activity(Label = "@string/add_product")]
public class AddProductView : MvxActivity<AddProductViewModel>
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
this.SetContentView(Resource.Layout.AddProductPage);
}
}
6. Test it.
using System.Collections.Generic;
using System.Windows.Input;
using Interfaces;
using Models;
using MvvmCross.Commands;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;
using Services;
467
public class RegisterViewModel : MvxViewModel
{
private readonly IApiService apiService;
private readonly IMvxNavigationService navigationService;
private readonly IDialogService dialogService;
private List<Country> countries;
private List<City> cities;
private Country selectedCountry;
private City selectedCity;
private MvxCommand registerCommand;
private string firstName;
private string lastName;
private string email;
private string address;
private string phone;
private string password;
private string confirmPassword;
public RegisterViewModel(
IMvxNavigationService navigationService,
IApiService apiService,
IDialogService dialogService)
{
this.apiService = apiService;
this.navigationService = navigationService;
this.dialogService = dialogService;
this.LoadCountries();
}
468
get
{
this.registerCommand = this.registerCommand ?? new MvxCommand(this.RegisterUser);
return this.registerCommand;
}
}
469
{
get => this.phone;
set => this.SetProperty(ref this.phone, value);
}
470
set
{
this.selectedCountry = value;
this.RaisePropertyChanged(() => SelectedCountry);
this.Cities = SelectedCountry.Cities;
}
}
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
this.Countries = (List<Country>)response.Result;
471
}
await this.navigationService.Close(this);
}
}
472
private MvxCommand registerCommand;
…
public ICommand RegisterCommand
{
get
{
this.registerCommand = this.registerCommand ?? new MvxCommand(this.DoRegisterCommand);
return this.registerCommand;
}
}
...
private async void DoRegisterCommand()
{
await this.navigationService.Navigate<RegisterViewModel>();
}
3. Now let’s create the login view, so we must look for the layout folder and create a new layout called
LoginPage.axml and then we are going to add the next code:
474
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:local="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="15dp">
<ImageView
android:id="@+id/imageViewEmail"
android:src="@drawable/shop"
android:layout_width="300dp"
android:layout_height="200dp"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<TextView
android:id="@+id/textViewEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
app:layout_constraintTop_toBottomOf="@+id/imageViewEmail"/>
<EditText
android:id="@+id/editTextEmail"
475
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Email"
app:layout_constraintTop_toBottomOf="@+id/textViewEmail"/>
<TextView
android:id="@+id/textViewPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Password"
app:layout_constraintTop_toBottomOf="@+id/editTextEmail"/>
<EditText
android:id="@+id/editTextPassword"
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Password"
app:layout_constraintTop_toBottomOf="@+id/textViewPassword"/>
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
476
app:layout_constraintRight_toRightOf="parent"
android:paddingBottom="10dp">
<Button
android:id="@+id/loginButton"
android:text="LOGIN"
android:layout_width="150dp"
android:layout_marginLeft="25dp"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:background="@drawable/primary_button"
local:MvxBind="Click LoginCommand"/>
<Button
android:id="@+id/newUserButton"
android:text="NEW USER"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginBottom="2dp"
android:background="@drawable/secondary_button"
local:MvxBind="Click RegisterCommand"/>
</LinearLayout>
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:layout_constraintTop_toTopOf="@+id/buttonContainer"
477
app:layout_constraintBottom_toTopOf="@+id/editTextPassword"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
</android.support.constraint.ConstraintLayout>
4. Let’s create a register page, let’s find the layout folder and then we are going to create a new layout called
RegisterPage.axml and then we add the code below:
<TextView
android:id="@+id/title_textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
local:layout_constraintLeft_toLeftOf="parent"
local:layout_constraintRight_toRightOf="parent"
android:text="Register"/>
478
<LinearLayout
android:id="@+id/firstName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/title_textView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="First name:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text FirstName"/>
</LinearLayout>
<LinearLayout
android:id="@+id/flastName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/firstName_layout">
479
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Last name:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text LastName"/>
</LinearLayout>
<LinearLayout
android:id="@+id/emailName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/flastName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Email:"/>
<EditText
480
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Email"/>
</LinearLayout>
<LinearLayout
android:id="@+id/countryName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/emailName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:textSize="18dp"
android:text="Country:"/>
<mvvmcross.platforms.android.binding.views.MvxSpinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="ItemsSource Countries;SelectedItem SelectedCountry"/>
</LinearLayout>
<LinearLayout
481
android:id="@+id/cityName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/countryName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="City:"/>
<mvvmcross.platforms.android.binding.views.MvxSpinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="ItemsSource Cities;SelectedItem SelectedCity"/>
</LinearLayout>
<LinearLayout
android:id="@+id/addressName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/cityName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
482
android:textSize="18dp"
android:text="Address:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Address"/>
</LinearLayout>
<LinearLayout
android:id="@+id/phoneName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/addressName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Phone:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
483
android:textSize="18dp"
local:MvxBind="Text Phone"/>
</LinearLayout>
<LinearLayout
android:id="@+id/passwordName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/phoneName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Password:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Password"/>
</LinearLayout>
<LinearLayout
android:id="@+id/confirmName_layout"
484
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/passwordName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Password confirm"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text ConfirmPassword"/>
</LinearLayout>
<Button
android:id="@+id/newUserButton"
android:text="REGISTER NEW USER"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
local:MvxBind="Click RegisterCommand"
local:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/primary_button"/>
485
</android.support.constraint.ConstraintLayout>
5. First we are going to create a new class on View folder called RegisterView.cs and let’s add the code below:
using Common.ViewModels;
using global::Android.App;
using global::Android.OS;
using MvvmCross.Platforms.Android.Views;
[Activity(Label = "@string/app_name")]
public class RegisterView : MvxActivity<RegisterViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.SetContentView(Resource.Layout.RegisterPage);
}
}
6. Test it.
await this.navigationService.Close(this);
2. Then avoid the load products from constructor and put this method:
486
public override void ViewAppeared()
{
base.ViewAppeared();
this.LoadProducts();
}
using System.Windows.Input;
using Helpers;
using Interfaces;
using Models;
using MvvmCross.Commands;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;
using Newtonsoft.Json;
using Services;
public ProductsDetailViewModel(
IApiService apiService,
IDialogService dialogService,
IMvxNavigationService navigationService)
{
this.apiService = apiService;
this.dialogService = dialogService;
this.navigationService = navigationService;
this.IsLoading = false;
}
488
{
get
{
this.updateCommand = this.updateCommand ?? new MvxCommand(this.Update);
return this.updateCommand;
}
}
489
"bearer",
token.Token);
this.IsLoading = false;
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
await this.navigationService.Close(this);
}
if (this.Product.Price <= 0)
{
this.dialogService.Alert("Error", "The price must be a number greather than zero.", "Accept");
return;
}
this.IsLoading = true;
490
var token = JsonConvert.DeserializeObject<TokenResponse>(Settings.Token);
this.IsLoading = false;
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
await this.navigationService.Close(this);
}
491
5. Add command to navigate when tap on the item:
492
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_margin="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:cardElevation="5dp"
local:cardCornerRadius="6dp">
<android.support.constraint.ConstraintLayout
android:layout_margin="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ffimageloading.cross.MvxCachedImageView
android:id="@+id/productImageView"
android:layout_width="100dp"
android:layout_height="100dp"
local:MvxBind="ImagePath ImageFullPath" />
<TextView
android:id="@+id/nameTextView"
android:textStyle="bold"
android:textSize="24dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Product Description"
493
local:layout_constraintLeft_toRightOf="@+id/productImageView"
local:MvxBind="Text Name" />
<TextView
android:id="@+id/priceTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="italic"
android:layout_marginTop="6dp"
android:textSize="36dp"
tools:text="$16.99"
local:layout_constraintTop_toBottomOf="@+id/nameTextView"
local:layout_constraintLeft_toRightOf="@+id/productImageView"
local:MvxBind="Text Price,Converter=DecimalToString" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
<android.support.design.widget.FloatingActionButton
494
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:src="@drawable/ic_add"
android:layout_margin="16dp"
local:layout_anchorGravity="bottom|right|end"
local:MvxBind="Click AddProductCommand" />
<mvvmcross.droid.support.v7.recyclerview.MvxRecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
local:layout_constraintTop_toBottomOf="@+id/toolbar_cross"
local:MvxItemTemplate="@layout/productrow"
local:MvxBind="ItemsSource Products; ItemClick ItemClickCommand;"/>
</android.support.design.widget.CoordinatorLayout>
495
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="15dp">
<LinearLayout
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ffimageloading.cross.MvxCachedImageView
android:id="@+id/productImageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
local:MvxBind="ImagePath Product.ImageFullPath" />
<TextView
android:text="Name"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
496
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Product.Name" />
<TextView
android:text="Price"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="numberDecimal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Product.Price" />
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
</LinearLayout>
<LinearLayout
497
android:id="@+id/buttonContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:paddingBottom="10dp">
<Button
android:id="@+id/updateButton"
android:text="Update"
android:layout_width="150dp"
android:layout_marginLeft="25dp"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:background="@drawable/primary_button"
local:MvxBind="Click UpdateCommand"/>
<Button
android:id="@+id/deleteButton"
android:text="Delete"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginBottom="2dp"
android:background="@drawable/danger_button"
local:MvxBind="Click DeleteCommand"/>
498
</LinearLayout>
</android.support.constraint.ConstraintLayout>
using Common.ViewModels;
using global::Android.App;
using global::Android.OS;
using MvvmCross.Platforms.Android.Views;
[Activity(Label = "@string/app_name")]
public class ProductDetailView : MvxActivity<ProductsDetailViewModel>
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
this.SetContentView(Resource.Layout.ProductDetailPage);
}
}
6. Test it.
499
{
bool IsConnectedToWifi();
}
2. In all the Views that you need to check do this. Inject the new interface, example in LoginView:
public LoginViewModel(
IApiService apiService,
IDialogService dialogService,
IMvxNavigationService navigationService,
INetworkProvider networkProvider)
{
this.apiService = apiService;
this.dialogService = dialogService;
this.navigationService = navigationService;
this.networkProvider = networkProvider;
this.Email = "jzuluaga55@gmail.com";
this.Password = "123456";
this.IsLoading = false;
}
if (!this.networkProvider.IsConnectedToWifi())
{
this.dialogService.Alert("Error", "You need internet connection to enter to the App.", "Accept");
return;
}
500
4. Finish in core.
using Common.Interfaces;
using global::Android.Content;
using global::Android.Net.Wifi;
using MvvmCross;
using MvvmCross.Platforms.Android;
501
private readonly Context context;
public NetworkProvider()
{
context = Mvx.Resolve<IMvxAndroidCurrentTopActivity>().Activity;
}
base.InitializeFirstChance();
}
4. Test it.
502
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>
<resources>
<string name="app_name">Shop</string>
<string name="add_product">Add Product</string>
<string name="register">Register New User</string>
<string name="product_details">Product Details</string>
<string name="products">Shop Products</string>
</resources>
<android.support.v7.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
503
android:background="?attr/colorPrimary"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" />
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:src="@drawable/ic_add"
android:layout_margin="16dp"
local:layout_anchorGravity="bottom|right|end"
local:MvxBind="Click AddProductCommand" />
<LinearLayout
android:id="@+id/header_layout"
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
504
<include
layout="@layout/toolbar" />
<mvvmcross.droid.support.v7.recyclerview.MvxRecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
local:layout_constraintTop_toBottomOf="@+id/toolbar_cross"
local:MvxItemTemplate="@layout/productrow"
local:MvxBind="ItemsSource Products;ItemClick ItemClickCommand;"/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
using Android.App;
using Android.OS;
using MvvmCross.Droid.Support.V7.AppCompat;
using Shop.Common.ViewModels;
using Toolbar = global::Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
{
[Activity(Label = "@string/products")]
public class ProductsView : MvxAppCompatActivity<ProductsViewModel>
{
505
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.SetContentView(Resource.Layout.ProductsPage);
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
}
}
}
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
506
layout="@layout/toolbar" />
<LinearLayout
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ffimageloading.cross.MvxCachedImageView
android:id="@+id/productImageView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
local:MvxBind="ImagePath Product.ImageFullPath" />
<TextView
android:text="Name"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Product.Name" />
507
<TextView
android:text="Price"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="numberDecimal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Product.Price" />
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
508
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:paddingBottom="10dp">
<Button
android:id="@+id/updateButton"
android:text="Update"
android:textColor="#FFFFFF"
android:layout_width="150dp"
android:layout_marginLeft="25dp"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:background="@drawable/primary_button"
local:MvxBind="Click UpdateCommand"/>
<Button
android:id="@+id/deleteButton"
android:text="Delete"
android:textColor="#FFFFFF"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginBottom="2dp"
android:background="@drawable/danger_button"
local:MvxBind="Click DeleteCommand"/>
</LinearLayout>
509
</android.support.constraint.ConstraintLayout>
using Android.App;
using Android.OS;
using Android.Views;
using MvvmCross.Droid.Support.V7.AppCompat;
using Shop.Common.ViewModels;
using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
{
[Activity(Label = "@string/product_details")]
public class ProductDetailView : MvxAppCompatActivity<ProductsDetailViewModel>
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
this.SetContentView(Resource.Layout.ProductDetailPage);
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
510
}
}
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == global::Android.Resource.Id.Home) { OnBackPressed(); }
return base.OnOptionsItemSelected(item);
}
}
}
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
511
layout="@layout/toolbar" />
<LinearLayout
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_gravity="center"
android:src="@drawable/noImage"
android:layout_width="300dp"
android:layout_height="200dp" />
<TextView
android:text="Name"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Name" />
512
<TextView
android:text="Price"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="numberDecimal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Price" />
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
local:layout_constraintBottom_toBottomOf="parent"
android:orientation="vertical"
android:paddingTop="10dp"
513
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingBottom="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/saveButton"
android:text="Save"
android:textColor="#FFFFFF"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
local:MvxBind="Click AddProductCommand"
android:background="@drawable/primary_button"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
using Android.Views;
using Android.App;
using Android.OS;
using MvvmCross.Droid.Support.V7.AppCompat;
using Shop.Common.ViewModels;
using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
{
514
[Activity(Label = "@string/add_product")]
public class AddProductView : MvxAppCompatActivity<AddProductViewModel>
{
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
this.SetContentView(Resource.Layout.AddProductPage);
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
515
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/toolbar" />
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:layout_constraintTop_toBottomOf="@+id/toolbar">
<LinearLayout
android:id="@+id/firstName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
516
android:textSize="18dp"
android:text="First name:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text FirstName"/>
</LinearLayout>
<LinearLayout
android:id="@+id/flastName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/firstName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Last name:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
517
android:textSize="18dp"
local:MvxBind="Text LastName"/>
</LinearLayout>
<LinearLayout
android:id="@+id/emailName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/flastName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Email:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Email"/>
</LinearLayout>
<LinearLayout
android:id="@+id/countryName_layout"
518
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/emailName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:textSize="18dp"
android:text="Country:"/>
<mvvmcross.platforms.android.binding.views.MvxSpinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="ItemsSource Countries;SelectedItem SelectedCountry"/>
</LinearLayout>
<LinearLayout
android:id="@+id/cityName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/countryName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
519
android:text="City:"/>
<mvvmcross.platforms.android.binding.views.MvxSpinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="ItemsSource Cities;;SelectedItem SelectedCity"/>
</LinearLayout>
<LinearLayout
android:id="@+id/addressName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/cityName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Address:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Address"/>
520
</LinearLayout>
<LinearLayout
android:id="@+id/phoneName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/addressName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Phone:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Phone"/>
</LinearLayout>
<LinearLayout
android:id="@+id/passwordName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
521
local:layout_constraintTop_toBottomOf="@+id/phoneName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Password:"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Password"/>
</LinearLayout>
<LinearLayout
android:id="@+id/confirmName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/passwordName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Password confirm"/>
522
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text ConfirmPassword"/>
</LinearLayout>
</LinearLayout>
<Button
android:layout_margin="10dp"
android:id="@+id/newUserButton"
android:text="Register New User"
android:textColor="#FFFFFF"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
local:MvxBind="Click RegisterCommand"
local:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/primary_button"/>
</android.support.constraint.ConstraintLayout>
using Android.Views;
using Android.App;
523
using Android.OS;
using MvvmCross.Droid.Support.V7.AppCompat;
using Shop.Common.ViewModels;
using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
{
[Activity(Label = "@string/register")]
public class RegisterView : MvxAppCompatActivity<RegisterViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.SetContentView(Resource.Layout.RegisterPage);
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
524
}
}
}
using System;
void Confirm(string title, string message, string okButtonTitle, string dismissButtonTitle, Action confirmed, Action
dismissed);
}
525
() => { this.ConfirmDelete(); },
null);
}
this.IsLoading = false;
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
await this.navigationService.Close(this);
}
526
3. Ready on core.
527
confirmed.Invoke();
}
});
builder.Show();
}
2. Test it.
Core Ninth Part (Change User, Change Password and Drawer Menu)
1. Add this method to the IDialogService:
void Alert(
string message,
string title,
string okbtnText,
Action confirmed);
if (!response.IsSuccess)
{
this.IsLoading = false;
this.dialogService.Alert("Error", "User or password incorrect.", "Accept");
return;
}
528
var response2 = await this.apiService.GetUserByEmailAsync(
"https://shopzulu.azurewebsites.net",
"/api",
"/Account/GetUserByEmail",
this.Email,
"bearer",
token.Token);
3. Modify the RegisterViewModel to make local validations and fix the problem with the message:
if (string.IsNullOrEmpty(this.LastName))
{
529
this.dialogService.Alert("Error", "You must enter a last name.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.Email))
{
this.dialogService.Alert("Error", "You must enter an email.", "Accept");
return;
}
if (!RegexHelper.IsValidEmail(this.Email))
{
this.dialogService.Alert("Error", "You must enter a valid email.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.Address))
{
this.dialogService.Alert("Error", "You must enter an address.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.Phone))
{
this.dialogService.Alert("Error", "You must enter a phone.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.Password))
530
{
this.dialogService.Alert("Error", "You must enter a pasword.", "Accept");
return;
}
if (this.Password.Length < 6)
{
this.dialogService.Alert("Error", "The password must be a least 6 characters.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.ConfirmPassword))
{
this.dialogService.Alert("Error", "You must enter a pasword confirm.", "Accept");
return;
}
if (!this.Password.Equals(this.ConfirmPassword))
{
this.dialogService.Alert("Error", "The pasword and confirm does not math.", "Accept");
return;
}
this.IsLoading = true;
531
Email = this.Email,
FirstName = this.FirstName,
LastName = this.LastName,
Password = this.Password,
Phone = this.Phone
};
this.IsLoading = false;
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
this.dialogService.Alert(
"Ok",
"The user was created succesfully, you must " +
"confirm your user by the email sent to you and then you could login with " +
"the email and password entered.",
"Accept",
() => { this.navigationService.Close(this); });
}
532
4. Add the EditUserViewModel:
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Interfaces;
using Models;
using MvvmCross.Commands;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;
using Newtonsoft.Json;
using Services;
using Shop.Common.Helpers;
public EditUserViewModel(
533
IMvxNavigationService navigationService,
IApiService apiService,
IDialogService dialogService)
{
this.apiService = apiService;
this.navigationService = navigationService;
this.dialogService = dialogService;
this.IsLoading = false;
this.User = JsonConvert.DeserializeObject<User>(Settings.User);
this.LoadCountries();
}
534
get => this.user;
set => this.SetProperty(ref this.user, value);
}
535
get => selectedCity;
set
{
selectedCity = value;
RaisePropertyChanged(() => SelectedCity);
}
}
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
this.Countries = (List<Country>)response.Result;
foreach (var country in this.Countries)
{
var city = country.Cities.Where(c => c.Id == this.User.CityId).FirstOrDefault();
if (city != null)
{
this.SelectedCountry = country;
this.SelectedCity = city;
536
break;
}
}
}
if (string.IsNullOrEmpty(this.User.LastName))
{
this.dialogService.Alert("Error", "You must enter a last name.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.User.Address))
{
this.dialogService.Alert("Error", "You must enter an address.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.User.PhoneNumber))
{
this.dialogService.Alert("Error", "You must enter a phone.", "Accept");
return;
537
}
this.IsLoading = true;
this.IsLoading = true;
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
Settings.User = JsonConvert.SerializeObject(this.User);
await this.navigationService.Close(this);
}
}
using System.Windows.Input;
538
using Helpers;
using Interfaces;
using Models;
using MvvmCross.Commands;
using MvvmCross.Navigation;
using MvvmCross.ViewModels;
using Newtonsoft.Json;
using Services;
public ChangePasswordViewModel(
IMvxNavigationService navigationService,
IApiService apiService,
IDialogService dialogService)
{
this.apiService = apiService;
this.navigationService = navigationService;
this.dialogService = dialogService;
this.IsLoading = false;
539
}
540
public string ConfirmPassword
{
get => this.confirmPassword;
set => this.SetProperty(ref this.confirmPassword, value);
}
if (!this.CurrentPassword.Equals(Settings.UserPassword))
{
this.dialogService.Alert("Error", "The current pasword is not correct.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.NewPassword))
{
this.dialogService.Alert("Error", "You must enter a new pasword.", "Accept");
return;
}
if (this.NewPassword.Length < 6)
{
541
this.dialogService.Alert("Error", "The new password must be a least 6 characters.", "Accept");
return;
}
if (string.IsNullOrEmpty(this.ConfirmPassword))
{
this.dialogService.Alert("Error", "You must enter a pasword confirm.", "Accept");
return;
}
if (!this.NewPassword.Equals(this.ConfirmPassword))
{
this.dialogService.Alert("Error", "The pasword and confirm does not math.", "Accept");
return;
}
this.IsLoading = true;
542
"/Account/ChangePassword",
request,
"bearer",
token.Token);
if (!response.IsSuccess)
{
this.dialogService.Alert("Error", response.Message, "Accept");
return;
}
Settings.UserPassword = this.NewPassword;
await this.navigationService.Close(this);
}
}
6. Finish on core.
Android Ninth Part (Change User, Change Password and Drawer Menu)
1. Implement the new Alert in the IDialogService:
public void Alert(string title, string message, string okbtnText, Action confirmed)
{
var top = Mvx.Resolve<IMvxAndroidCurrentTopActivity>();
var act = top.Activity;
543
adb.SetMessage(message);
adb.SetPositiveButton(okbtnText, (sender, args) =>
{
if (confirmed != null)
{
confirmed.Invoke();
}
});
adb.Create().Show();
}
<resources>
<string name="app_name">Shop</string>
<string name="add_product">Add Product</string>
<string name="register">Register New User</string>
<string name="product_details">Product Details</string>
<string name="products">Shop Products</string>
<string name="edit_user">Edit User</string>
<string name="change_password">Change Password</string>
<string name="navigation_drawer_open">navigation_drawer_open</string>
<string name="navigation_drawer_closed">navigation_drawer_closed</string>
</resources>
544
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:local="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/mainContainer"
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
layout="@layout/toolbar" />
<LinearLayout
android:id="@+id/controlsContainer"
android:orientation="vertical"
android:paddingTop="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
545
<ImageView
android:layout_gravity="center"
android:src="@drawable/noImage"
android:layout_width="300dp"
android:layout_height="200dp" />
<TextView
android:text="Name"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Name" />
<TextView
android:text="Price"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="25px"
android:minHeight="25px"/>
546
<EditText
android:inputType="numberDecimal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Price" />
</LinearLayout>
</LinearLayout>
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
app:layout_constraintBottom_toTopOf="@+id/saveButton"
app:layout_constraintTop_toBottomOf="@+id/mainContainer"
android:keepScreenOn="true"/>
<Button
android:id="@+id/saveButton"
android:layout_margin="10dp"
android:text="Save"
android:textColor="#FFFFFF"
local:layout_constraintBottom_toBottomOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
local:MvxBind="Click AddProductCommand"
android:background="@drawable/primary_button"/>
547
</android.support.constraint.ConstraintLayout>
<include
layout="@layout/toolbar" />
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:layout_constraintTop_toBottomOf="@+id/toolbar">
<LinearLayout
android:id="@+id/currentPasswordName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
548
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/phoneName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Current Password:"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text CurrentPassword"/>
</LinearLayout>
<LinearLayout
android:id="@+id/newPasswordName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/phoneName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
549
android:textSize="18dp"
android:text="New Password:"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text NewPassword"/>
</LinearLayout>
<LinearLayout
android:id="@+id/confirmName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/passwordName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Password Confirm"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
550
android:textSize="18dp"
local:MvxBind="Text ConfirmPassword"/>
</LinearLayout>
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
</LinearLayout>
<Button
android:layout_margin="10dp"
android:id="@+id/changePasswordButton"
android:text="Change Password"
android:textColor="#FFFFFF"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
local:MvxBind="Click ChangePasswordCommand"
local:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/primary_button"/>
</android.support.constraint.ConstraintLayout>
551
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/toolbar" />
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:layout_constraintTop_toBottomOf="@+id/toolbar">
<LinearLayout
android:id="@+id/firstName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
552
android:textSize="18dp"
android:text="First name:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text User.FirstName"/>
</LinearLayout>
<LinearLayout
android:id="@+id/flastName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/firstName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Last name:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
553
android:textSize="18dp"
local:MvxBind="Text User.LastName"/>
</LinearLayout>
<LinearLayout
android:id="@+id/countryName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/emailName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:textSize="18dp"
android:text="Country:"/>
<mvvmcross.platforms.android.binding.views.MvxSpinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="ItemsSource Countries;SelectedItem SelectedCountry"/>
</LinearLayout>
<LinearLayout
android:id="@+id/cityName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
554
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/countryName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="City:"/>
<mvvmcross.platforms.android.binding.views.MvxSpinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="ItemsSource Cities;;SelectedItem SelectedCity"/>
</LinearLayout>
<LinearLayout
android:id="@+id/addressName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/cityName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Address:"/>
555
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text User.Address"/>
</LinearLayout>
<LinearLayout
android:id="@+id/phoneName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/addressName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Phone:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text User.PhoneNumber"/>
556
</LinearLayout>
<ProgressBar
android:layout_margin="10dp"
android:layout_height="wrap_content"
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
</LinearLayout>
<Button
android:layout_margin="10dp"
android:id="@+id/saveButton"
android:text="Save"
android:textColor="#FFFFFF"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
local:MvxBind="Click SaveCommand"
local:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/primary_button"/>
</android.support.constraint.ConstraintLayout>
557
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:local="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingTop="15dp">
<ImageView
android:id="@+id/imageViewEmail"
android:src="@drawable/shop"
android:layout_width="300dp"
android:layout_height="200dp"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<TextView
android:id="@+id/textViewEmail"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
app:layout_constraintTop_toBottomOf="@+id/imageViewEmail"/>
<EditText
android:id="@+id/editTextEmail"
android:inputType="textEmailAddress"
android:layout_width="match_parent"
558
android:layout_height="wrap_content"
local:MvxBind="Text Email"
app:layout_constraintTop_toBottomOf="@+id/textViewEmail"/>
<TextView
android:id="@+id/textViewPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Password"
app:layout_constraintTop_toBottomOf="@+id/editTextEmail"/>
<EditText
android:id="@+id/editTextPassword"
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Password"
app:layout_constraintTop_toBottomOf="@+id/textViewPassword"/>
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:paddingBottom="10dp">
559
<Button
android:id="@+id/loginButton"
android:text="LOGIN"
android:textColor="#FFFFFF"
android:layout_width="130dp"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
android:background="@drawable/primary_button"
local:MvxBind="Click LoginCommand"/>
<Button
android:id="@+id/newUserButton"
android:text="NEW USER"
android:layout_width="130dp"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_marginBottom="2dp"
android:background="@drawable/secondary_button"
local:MvxBind="Click RegisterCommand"/>
</LinearLayout>
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:layout_constraintBottom_toTopOf="@+id/buttonContainer"
app:layout_constraintTop_toBottomOf="@+id/editTextPassword"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
560
</android.support.constraint.ConstraintLayout>
<include
layout="@layout/toolbar" />
<LinearLayout
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:layout_constraintTop_toBottomOf="@+id/toolbar">
<LinearLayout
android:id="@+id/firstName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
561
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="First name:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text FirstName"/>
</LinearLayout>
<LinearLayout
android:id="@+id/flastName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/firstName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
562
android:text="Last name:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text LastName"/>
</LinearLayout>
<LinearLayout
android:id="@+id/emailName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/flastName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Email:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
563
local:MvxBind="Text Email"/>
</LinearLayout>
<LinearLayout
android:id="@+id/countryName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/emailName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:textSize="18dp"
android:text="Country:"/>
<mvvmcross.platforms.android.binding.views.MvxSpinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="ItemsSource Countries;SelectedItem SelectedCountry"/>
</LinearLayout>
<LinearLayout
android:id="@+id/cityName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
564
local:layout_constraintTop_toBottomOf="@+id/countryName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="City:"/>
<mvvmcross.platforms.android.binding.views.MvxSpinner
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="ItemsSource Cities;;SelectedItem SelectedCity"/>
</LinearLayout>
<LinearLayout
android:id="@+id/addressName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/cityName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Address:"/>
<EditText
565
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Address"/>
</LinearLayout>
<LinearLayout
android:id="@+id/phoneName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/addressName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Phone:"/>
<EditText
android:inputType="textEmailAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Phone"/>
</LinearLayout>
566
<LinearLayout
android:id="@+id/passwordName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/phoneName_layout">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Password:"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text Password"/>
</LinearLayout>
<LinearLayout
android:id="@+id/confirmName_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
local:layout_constraintTop_toBottomOf="@+id/passwordName_layout">
567
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18dp"
android:text="Password confirm"/>
<EditText
android:inputType="textPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18dp"
local:MvxBind="Text ConfirmPassword"/>
</LinearLayout>
<ProgressBar
android:layout_height="wrap_content"
android:layout_width="match_parent"
local:MvxBind="Visibility Visibility(IsLoading)"
android:indeterminateOnly="true"
android:keepScreenOn="true"/>
</LinearLayout>
<Button
android:layout_margin="10dp"
android:id="@+id/newUserButton"
android:text="Register New User"
android:textColor="#FFFFFF"
568
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="2dp"
local:MvxBind="Click RegisterCommand"
local:layout_constraintBottom_toBottomOf="parent"
android:background="@drawable/primary_button"/>
</android.support.constraint.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/toolbar" />
<FrameLayout
android:id="@+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
569
</LinearLayout>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="80dp">
<android.support.design.widget.FloatingActionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:src="@drawable/ic_add"
android:layout_margin="16dp"
local:layout_anchorGravity="bottom|right|end"
local:MvxBind="Click AddProductCommand" />
<LinearLayout
android:id="@+id/header_layout"
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<mvvmcross.droid.support.v7.recyclerview.MvxRecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
570
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
local:layout_constraintTop_toBottomOf="@+id/toolbar_cross"
local:MvxItemTemplate="@layout/productrow"
local:MvxBind="ItemsSource Products;ItemClick ItemClickCommand;"/>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
<ListView
android:id="@+id/drawerListView"
android:layout_gravity="start"
android:choiceMode="singleChoice"
android:layout_width="240dp"
android:focusable="false"
android:focusableInTouchMode="false"
android:layout_height="match_parent"
android:background="?android:attr/windowBackground" />
</android.support.v4.widget.DrawerLayout>
using Android.Views;
using Android.App;
using Android.OS;
using MvvmCross.Droid.Support.V7.AppCompat;
using Shop.Common.ViewModels;
using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
571
{
[Activity(Label = "@string/change_password")]
public class ChangePasswordView : MvxAppCompatActivity<ChangePasswordViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.SetContentView(Resource.Layout.ChangePasswordPage);
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
using Android.Views;
572
using Android.App;
using Android.OS;
using MvvmCross.Droid.Support.V7.AppCompat;
using Shop.Common.ViewModels;
using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
{
[Activity(Label = "@string/edit_user")]
public class EditUserView : MvxAppCompatActivity<EditUserViewModel>
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.SetContentView(Resource.Layout.EditUserPage);
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
573
}
}
}
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Support.V4.Widget;
using Android.Support.V7.App;
using Android.Views;
using Android.Widget;
using MvvmCross.Droid.Support.V7.AppCompat;
using Shop.Common.ViewModels;
using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
{
[Activity(
Label = "@string/products",
ScreenOrientation = ScreenOrientation.Portrait)]
public class ProductsView : MvxAppCompatActivity<ProductsViewModel>
{
private readonly string[] menuOptions = { "Add Product", "Edit User", "Change Password", "Close Session" };
private ListView drawerListView;
private DrawerLayout drawer;
private ActionBarDrawerToggle toggle;
574
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
this.SetContentView(Resource.Layout.ProductsPage);
var toolbar = FindViewById<Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
SupportActionBar.SetHomeAsUpIndicator(Resource.Drawable.menu_icon);
SupportActionBar.SetDisplayHomeAsUpEnabled(true);
SupportActionBar.SetDisplayShowHomeEnabled(true);
drawerListView = FindViewById<ListView>(Resource.Id.drawerListView);
drawerListView.Adapter = new ArrayAdapter<string>(this, global::Android.Resource.Layout.SimpleListItem1,
menuOptions);
drawerListView.ItemClick += listView_ItemClick;
drawer = FindViewById<DrawerLayout>(Resource.Id.drawerLayout);
toggle = new ActionBarDrawerToggle(
this,
drawer,
toolbar,
Resource.String.navigation_drawer_open,
Resource.String.navigation_drawer_closed);
drawer.AddDrawerListener(toggle);
toggle.SyncState();
}
575
StartActivity(typeof(AddProductView));
break;
case 1:
StartActivity(typeof(EditUserView));
break;
case 2:
StartActivity(typeof(ChangePasswordView));
break;
case 3:
OnBackPressed();
break;
}
drawer.CloseDrawer(drawerListView);
}
return base.OnOptionsItemSelected(item);
}
}
}
576
Core Tenth Part (Taking images from camera or gallery)
1. Add the interfaz IMvxPictureChooserTask:
using System;
using System.IO;
using System;
using System.Collections.Generic;
using System.IO;
577
{
bool TryReadTextFile(string path, out string contents);
using System;
578
{
void TakeNewPhoto(Action<byte[]> onSuccess, Action<string> onError);
public AddProductViewModel(
IApiService apiService,
IDialogService dialogService,
IMvxNavigationService navigationService,
IPictureService pictureService)
{
this.apiService = apiService;
this.dialogService = dialogService;
this.navigationService = navigationService;
this.pictureService = pictureService;
579
}
580
() => { this.pictureService.TakeNewPhoto(ProcessPhoto, null); },
() => { this.pictureService.SelectExistingPicture(ProcessPhoto, null); });
}
using global::Android.Graphics;
using MvvmCross.Converters;
using System;
using System.Globalization;
581
return BitmapFactory.DecodeByteArray(value, 0, value.Length); ;
}
}
7. Ready on core.
using System;
582
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Common.Interfaces;
583
public void DeleteFile(string filePath)
{
var fullPath = FullPath(filePath);
File.Delete(fullPath);
}
584
if (binaryReader.Read(memoryBuffer, 0,
memoryBuffer.Length) !=
memoryBuffer.Length)
{
return false; // TODO - do more here?
}
result = memoryBuffer;
return true;
}
});
contents = result;
return toReturn;
}
585
});
}
586
}
if (File.Exists(fullTo))
{
if (deleteExistingTo)
{
File.Delete(fullTo);
}
else
{
return false;
}
}
File.Move(fullFrom, fullTo);
return true;
}
catch (ThreadAbortException)
{
throw;
}
catch (Exception)
{
return false;
}
}
587
{
try
{
var fullPath = FullPath(path);
if (File.Exists(fullPath))
{
File.Delete(fullPath);
}
using (var fileStream = File.OpenWrite(fullPath))
{
streamAction(fileStream);
}
}
catch (Exception ex)
{
throw ex;
}
}
588
}
}
}
using System.IO;
using global::Android.Content;
using MvvmCross;
using MvvmCross.Platforms.Android;
589
}
}
using System;
using System.IO;
using Common.Interfaces;
using global::Android.App;
using global::Android.Content;
using global::Android.Graphics;
using global::Android.Provider;
using MvvmCross;
using MvvmCross.Exceptions;
using MvvmCross.Platforms.Android;
using MvvmCross.Platforms.Android.Views.Base;
using Uri = global::Android.Net.Uri;
590
var intent = new Intent(Intent.ActionGetContent);
intent.SetType("image/*");
ChoosePictureCommon(MvxIntentRequestCode.PickFromFile, intent, maxPixelDimension, percentQuality,
pictureAvailable, assumeCancelled);
}
591
return
Mvx.Resolve<IMvxAndroidGlobals>().ApplicationContext.ContentResolver.Insert(MediaStore.Images.Media.ExternalCont
entUri, contentValues);
}
592
}
ProcessPictureUri(result, uri);
}
593
finally
{
if (!responseSent)
{
_currentRequestParameters.AssumeCancelled();
}
_currentRequestParameters = null;
}
}
594
{
// this shouldn't happen, but if it does... then trace the error and set sampleSize to 1
sampleSize = 1;
}
595
}
using global::Android.Content;
using global::Android.Util;
using global::Android.Widget;
596
public class MyImageView : ImageView
{
public MyImageView(Context context, IAttributeSet attrs) : base(context, attrs)
{
}
}
using System;
using System.IO;
using Common.Interfaces;
using MvvmCross;
597
pictureStream.CopyTo(memoryStream);
onSuccess(memoryStream.GetBuffer());
},
() => { /* cancel is ignored */ });
}
598
catch (Exception)
{
fileName = null;
}
return fileName;
}
}
if (global::Android.Support.V4.App.ActivityCompat.CheckSelfPermission(this,
global::Android.Manifest.Permission.Camera) != global::Android.Content.PM.Permission.Granted ||
global::Android.Support.V4.App.ActivityCompat.CheckSelfPermission(this,
global::Android.Manifest.Permission.WriteExternalStorage) != global::Android.Content.PM.Permission.Granted ||
global::Android.Support.V4.App.ActivityCompat.CheckSelfPermission(this,
global::Android.Manifest.Permission.ReadExternalStorage) != global::Android.Content.PM.Permission.Granted)
{
global::Android.Support.V4.App.ActivityCompat.RequestPermissions(this, new string[] {
global::Android.Manifest.Permission.Camera, global::Android.Manifest.Permission.WriteExternalStorage,
global::Android.Manifest.Permission.ReadExternalStorage }, 1);
}
599
var actionBar = SupportActionBar;
if (actionBar != null)
{
actionBar.SetDisplayHomeAsUpEnabled(true);
}
}
<Shop.UICross.Android.Services.MyImageView
android:layout_gravity="center"
android:src="@drawable/noImage"
android:layout_width="300dp"
local:MvxBind="Click SelectPictureCommand;Bitmap ByteArrayToImage(TheRawImageBytes)"
android:layout_height="200dp" />
base.InitializeFirstChance();
}
600
10. Test it!
3. Modify the AndroidManifest.xml. Note: you can get your own Google API Key from:
https://console.developers.google.com/apis):
601
</application>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
<resources>
<string name="app_name">Shop</string>
<string name="add_product">Add Product</string>
<string name="register">Register New User</string>
<string name="product_details">Product Details</string>
<string name="products">Shop Products</string>
<string name="edit_user">Edit User</string>
<string name="change_password">Change Password</string>
<string name="navigation_drawer_open">navigation_drawer_open</string>
<string name="navigation_drawer_closed">navigation_drawer_closed</string>
<string name="maps">Maps</string>
</resources>
602
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:local="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/mainContainer"
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
layout="@layout/toolbar" />
<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.google.android.gms.maps.MapFragment"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
603
6. Add the MapsView:
using Android.App;
using Android.Gms.Maps;
using Android.OS;
using Android.Views;
using MvvmCross.Droid.Support.V7.AppCompat;
using System;
using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
{
[Activity(Label = "@string/maps")]
public class MapsView : MvxAppCompatActivity, IOnMapReadyCallback
{
private GoogleMap googlemap;
604
if (actionBar != null)
{
actionBar.SetDisplayHomeAsUpEnabled(true);
}
[Obsolete]
private void SetUpMap()
{
if (googlemap == null)
{
FragmentManager.FindFragmentById<MapFragment>(Resource.Id.map).GetMapAsync(this);
}
}
public void OnMapReady(GoogleMap googleMap)
{
googlemap = googleMap;
}
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == global::Android.Resource.Id.Home)
{
OnBackPressed();
}
return base.OnOptionsItemSelected(item);
}
}
}
605
7. Modify the ProductsView
private readonly string[] menuOptions = { "Add Product", "Edit User", "Change Password", "Maps", "Close Session" };
…
case 2:
StartActivity(typeof(ChangePasswordView));
break;
case 3:
StartActivity(typeof(MapsView));
break;
case 4:
OnBackPressed();
break;
<LinearLayout
606
android:id="@+id/mainContainer"
android:orientation="vertical"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include
layout="@layout/toolbar" />
<TextView
android:layout_marginTop="20dp"
android:textSize="26dp"
android:text="You are at"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/txtCoordinates"
android:layout_marginTop="20dp"
android:textSize="26dp"
android:layout_gravity="center_horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnGetCoord"
android:text="Get Coordinates"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
607
<Button
android:id="@+id/btnTrackingLocation"
android:text="Start Location Update"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.google.android.gms.maps.MapFragment"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
using Android;
using Android.App;
using Android.Content.PM;
using Android.Gms.Common;
using Android.Gms.Common.Apis;
using Android.Gms.Location;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
using Android.Locations;
using Android.OS;
using Android.Runtime;
using Android.Support.V4.App;
608
using Android.Views;
using Android.Widget;
using MvvmCross.Droid.Support.V7.AppCompat;
using System;
using static Android.Gms.Common.Apis.GoogleApiClient;
using ILocationListener = Android.Gms.Location.ILocationListener;
using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace Shop.UICross.Android.Views
{
[Activity(Label = "@string/maps")]
public class MapsView : MvxAppCompatActivity, IOnMapReadyCallback, IConnectionCallbacks,
IOnConnectionFailedListener, ILocationListener
{
private GoogleMap googlemap;
private const int MY_PERMISSION_REQUEST_CODE = 7171;
private const int PLAY_SERVICES_RESOLUTION_REQUEST = 7172;
private TextView txtCoordinates;
private Button btnGetCoordinates, btnTracking;
private bool mRequestingLocationUpdates = false;
private LocationRequest mLocationRequest;
private GoogleApiClient mGoogleApiClient;
private Location mLastLocation;
609
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum]
Permission[] grantResults)
{
switch (requestCode)
{
case MY_PERMISSION_REQUEST_CODE:
if (grantResults.Length > 0 && grantResults[0] == Permission.Granted)
{
if (CheckPlayServices())
{
BuildGoogleApiClient();
CreateLocationRequest();
}
}
break;
}
}
txtCoordinates = FindViewById<TextView>(Resource.Id.txtCoordinates);
btnGetCoordinates = FindViewById<Button>(Resource.Id.btnGetCoord);
btnTracking = FindViewById<Button>(Resource.Id.btnTrackingLocation);
610
if (ActivityCompat.CheckSelfPermission(this, Manifest.Permission.AccessFineLocation) != Permission.Granted
&& ActivityCompat.CheckSelfPermission(this, Manifest.Permission.AccessCoarseLocation) !=
Permission.Granted)
{
ActivityCompat.RequestPermissions(this, new string[] {
Manifest.Permission.AccessCoarseLocation,
Manifest.Permission.AccessFineLocation
}, MY_PERMISSION_REQUEST_CODE);
}
else
{
if (CheckPlayServices())
{
BuildGoogleApiClient();
CreateLocationRequest();
}
}
btnGetCoordinates.Click += delegate
{
DisplayLocation();
};
btnTracking.Click += delegate
{
TogglePeriodicLocationUpdates();
};
611
SetSupportActionBar(toolbar);
[Obsolete]
private void SetUpMap()
{
if (googlemap == null)
{
FragmentManager.FindFragmentById<MapFragment>(Resource.Id.map).GetMapAsync(this);
}
}
612
}
613
mLocationRequest = new LocationRequest();
mLocationRequest.SetInterval(UPDATE_INTERVAL);
mLocationRequest.SetFastestInterval(FATEST_INTERVAL);
mLocationRequest.SetPriority(LocationRequest.PriorityHighAccuracy);
mLocationRequest.SetSmallestDisplacement(DISPLACEMENT);
}
614
Toast.MakeText(ApplicationContext, "This device is not support Google Play Services",
ToastLength.Long).Show();
Finish();
}
return false;
}
return true;
}
615
}
616
}
617
2. Modify the interfaz IDialogService adding the following methods:
void CustomAlert(
DialogType dialogType,
string title,
string message,
string okbtnText,
Action confirmed);
void CustomAlert(
DialogType dialogType,
string title,
string message,
string okButtonTitle,
string dismissButtonTitle,
Action confirmed,
Action dismissed);
if (!response.IsSuccess)
{
this.IsLoading = false;
this.dialogService.CustomAlert(
DialogType.Warning,
"Error",
"User or password incorrect.",
"Accept",
null);
618
return;
}
5. Ready on core.
619
android:layout_height="fill_parent">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@android:color/holo_blue_dark"
android:gravity="center_vertical"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:text="Alert"
android:textColor="#fff"
android:textSize="18sp"
android:textStyle="bold" />
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:local="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFF"
android:paddingBottom="16dp"
android:layout_below="@+id/title"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true">
620
<ImageView
android:id="@+id/image"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#00FFFFFF"
android:layout_centerHorizontal="true"
android:layout_marginTop="15dp"
android:scaleType="centerCrop"
android:contentDescription="image"
android:src="@drawable/alert" />
<TextView
android:id="@+id/textViewMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/image"
android:layout_marginTop="10dp"
android:textColor="#000"
android:gravity="center"
android:text="User or Password Incorrect"
android:textSize="18dp"/>
<Button
android:text="Accept"
android:textColor="@android:color/white"
android:layout_below="@+id/body"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
621
android:layout_marginTop="160dp"
android:layout_marginBottom="2dp"
android:id="@+id/btnOK"
android:background="@drawable/primary_button"/>
</RelativeLayout>
</RelativeLayout>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:local="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@android:color/holo_blue_dark"
android:gravity="center_vertical"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:text="Alert"
android:textColor="#fff"
android:textSize="18sp"
622
android:textStyle="bold" />
<android.support.constraint.ConstraintLayout
android:id="@+id/body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFF"
android:paddingBottom="16dp"
android:layout_below="@+id/title"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true">
<ImageView
android:id="@+id/image"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#00FFFFFF"
android:layout_marginTop="15dp"
android:scaleType="centerCrop"
android:contentDescription="image"
android:src="@drawable/alert"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
<TextView
android:id="@+id/textViewMessage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/image"
623
android:layout_marginTop="10dp"
android:textColor="#000"
app:layout_constraintTop_toBottomOf="@+id/image"
android:gravity="center"
android:text="Are you gay?"
android:textSize="18dp"/>
<LinearLayout
android:layout_below="@+id/body"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_width="wrap_content"
android:layout_centerHorizontal="true"
android:layout_height="wrap_content">
<Button
android:text="Yes"
android:textColor="@android:color/white"
android:layout_width="130dp"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="160dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="2dp"
android:id="@+id/btnOne"
android:background="@drawable/primary_button"/>
<Button
android:text="No"
624
android:textColor="@android:color/white"
android:layout_width="130dp"
android:layout_height="wrap_content"
android:layout_marginTop="160dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="2dp"
android:id="@+id/btnTwo"
android:background="@drawable/danger_button"/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>
</RelativeLayout>
625
var okButton = customView.FindViewById<Button>(Android.Resource.Id.btnOK);
var titleTextView = customView.FindViewById<TextView>(Android.Resource.Id.title);
var imageImageView = customView.FindViewById<ImageView>(Android.Resource.Id.image);
var messageTextView = customView.FindViewById<TextView>(Android.Resource.Id.textViewMessage);
imageImageView.SetImageResource(this.GetImageId(dialogType));
titleTextView.Text = title;
messageTextView.Text = message;
okButton.Text = okbtnText;
okButton.Click += delegate
{
if (confirmed != null)
{
confirmed.Invoke();
}
else
{
builder.Dismiss();
}
};
builder.SetView(customView);
builder.Show();
}
626
string message,
string okButtonTitle,
string dismissButtonTitle,
Action confirmed,
Action dismissed)
{
var top = Mvx.IoCProvider.Resolve<IMvxAndroidCurrentTopActivity>();
var act = top.Activity;
AlertDialog builder = new AlertDialog.Builder(act).Create();
builder.SetCancelable(false);
imageImageView.SetImageResource(this.GetImageId(dialogType));
titleTextView.Text = title;
messageTextView.Text = message;
oneButton.Text = okButtonTitle;
twoButton.Text = dismissButtonTitle;
oneButton.Click += delegate
{
if (confirmed != null)
{
627
confirmed.Invoke();
}
else
{
builder.Dismiss();
}
};
twoButton.Click += delegate
{
if (dismissed != null)
{
dismissed.Invoke();
}
else
{
builder.Dismiss();
}
};
builder.SetView(customView);
builder.Show();
}
628
return (int)typeof(Android.Resource.Drawable).GetField("alert").GetValue(null);
case DialogType.Information:
return (int)typeof(Android.Resource.Drawable).GetField("information").GetValue(null);
case DialogType.Question:
return (int)typeof(Android.Resource.Drawable).GetField("question").GetValue(null);
case DialogType.Warning:
return (int)typeof(Android.Resource.Drawable).GetField("warning").GetValue(null);
default:
return (int)typeof(Android.Resource.Drawable).GetField("alert").GetValue(null);
}
}
5. Test it.
629