MAUI加Blazor做一个跨平台的记账APP(四)表单
上一篇记录了请求后端接口的数据,用户信息是直接传递到后端,这一篇会记录一下修改登录页面用表单的方式传递用户输入的信息。
期望的功能为:校验表单,登录失败或成功都给予提示,登录成功后跳转,登录成功后再次切换到login页面不再展示表单而是直接显示用户名。
在Login.razor.cs中添加引用,可用上内置的表单和校验,并添加一个类用于绑定表单的字段,同时也能定义上校验的规则
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components.Forms;
......
public UserLoginViewModel LoginModel = new();
public class UserLoginViewModel
{
[Required(ErrorMessage = "用户名不能为空")]
[StringLength(16, ErrorMessage = "用户名不应超出16个字符长度")]
public string UserName { get; set; }
[Required(ErrorMessage = "密码不能为空")]
public string Password { get; set; }
}
然后修改login.razor,这里用了EditForm,并绑定刚才定义的UserLoginViewModel,定义了用户名输入框和密码输入框和提交按钮,然后绑定到事件OnValidSubmit="LoginWithInput",这代表表单通过校验才会跳至该方法来处理逻辑。DataAnnotationsValidator和ValidationMessage则是用来校验的。
<h3>Test Login</h3>
<div class="row">
<div class="col-6">
<EditForm Model="LoginModel" OnValidSubmit="LoginWithInput">
<DataAnnotationsValidator />
<div class="mt-2">
<label class="col-2 form-label">用户名:</label>
<InputText @bind-Value="LoginModel.UserName" class="form-control col-10" />
<ValidationMessage For="()=>LoginModel.UserName" />
</div>
<div class="mt-2">
<label class="col-2 form-label">密码:</label>
<InputText type="password" @bind-Value="LoginModel.Password" class="form-control col-10" />
<ValidationMessage For="()=>LoginModel.Password" />
</div>
<div class="mt-2">
<button type="submit" class="btn btn-primary">
登录
</button>
</div>
</EditForm>
</div>
</div>
然后添加方法用于表单提交后的处理,失败停留在页面,成功则跳转账户页面。
public async Task LoginWithInput(EditContext context)
{
Console.WriteLine("start to login...");
token = await _userService.GetTokenAsync(LoginModel.UserName, LoginModel.Password);
Console.WriteLine($"Token is: {token}");
if (!string.IsNullOrWhiteSpace(token))
{
formMessage = "登录成功, 即将跳转";
await Task.Delay(500);
NavigationManager.NavigateTo("/accounts");
}
else
{
errorMessage = "登录失败";
}
}
要记得UserService需要修改为通过传入的参数去请求后端
public async Task<String> GetTokenAsync(string userName, string passWord)
......
var loginData = new
{
username = userName,
password = passWord
};
var loginContent = new StringContent(JsonSerializer.Serialize(loginData), Encoding.UTF8, "application/json");
......
此时还需要加上获取登录状态、用户名,根据登录情况展示信息,都是小修改,放在下面完整的文件内容中。
IUserService.cs
namespace accountingMAUIBlazor.Services;
public interface IUserService
{
Task<String> GetTokenAsync(string userName, string password);
bool CheckLoggedStatus();
string CheckLoginName();
}
UserService.cs
using System.Text.Json;
using System.Text;
using accountingMAUIBlazor.Models;
namespace accountingMAUIBlazor.Services;
public class UserService : IUserService
{
public bool IsLoggedIn { get; set; } = false;
public string loginName;
private string _token = string.Empty;
public bool CheckLoggedStatus()
{
return IsLoggedIn;
}
public string CheckLoginName()
{
return loginName;
}
public async Task<String> GetTokenAsync(string userName, string passWord)
{
if (!string.IsNullOrWhiteSpace(_token) || IsLoggedIn)
{
Console.WriteLine("no need to request api, return existent token");
return _token;
}
using var httpClient = new HttpClient();
HttpResponseMessage response;
try
{
var loginData = new
{
username = userName,
password = passWord
};
var loginContent = new StringContent(JsonSerializer.Serialize(loginData), Encoding.UTF8, "application/json");
response =
await httpClient.PostAsync("http://127.0.0.1:8000/external/api/login/", loginContent);
response.EnsureSuccessStatusCode();
}
catch (Exception e)
{
Console.WriteLine("Error when request api: " + e.Message);
return _token;
}
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine("result: " + result);
try
{
_token = JsonSerializer.Deserialize<Token>(result)?.token ?? throw new JsonException();
loginName = JsonSerializer.Deserialize<Token>(result)?.username ?? throw new JsonException();
IsLoggedIn = true;
}
catch (Exception e)
{
Console.WriteLine("Error when handle json: " + e.Message);
return _token;
}
return _token;
}
}
Login.razor
@page "/login"
@inject NavigationManager NavigationManager
@inject IUserService _userService
<h3>Test Login</h3>
@if (@isLoggedIn)
{
<div class="alert alert-success">你好 @loginName, 你已经登录过了</div>
}
else
{
<div class="row">
<div class="col-6">
@if (!string.IsNullOrWhiteSpace(@errorMessage))
{
<div class="alert alert-danger">@errorMessage</div>
}
@if (!string.IsNullOrWhiteSpace(@formMessage))
{
<div class="alert alert-success">@formMessage</div>
}
<EditForm Model="LoginModel" OnValidSubmit="LoginWithInput">
<DataAnnotationsValidator />
<div class="mt-2">
<label class="col-2 form-label">用户名:</label>
<InputText @bind-Value="LoginModel.UserName" class="form-control col-10" />
<ValidationMessage For="()=>LoginModel.UserName" />
</div>
<div class="mt-2">
<label class="col-2 form-label">密码:</label>
<InputText type="password" @bind-Value="LoginModel.Password" class="form-control col-10" />
<ValidationMessage For="()=>LoginModel.Password" />
</div>
<div class="mt-2">
<button type="submit" class="btn btn-primary">
登录
</button>
</div>
</EditForm>
</div>
</div>
}
Login.razor.cs
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Components.Forms;
namespace accountingMAUIBlazor.Pages;
public partial class Login
{
public bool isLoggedIn;
public string loginName;
private string token;
public string formMessage;
public string errorMessage;
public UserLoginViewModel LoginModel = new();
protected override void OnInitialized()
{
isLoggedIn = _userService.CheckLoggedStatus();
if (isLoggedIn)
{
loginName = _userService.CheckLoginName();
}
}
public async Task LoginWithInput(EditContext context)
{
Console.WriteLine("start to login...");
token = await _userService.GetTokenAsync(LoginModel.UserName, LoginModel.Password);
Console.WriteLine($"Token is: {token}");
if (!string.IsNullOrWhiteSpace(token))
{
formMessage = "登录成功, 即将跳转";
await Task.Delay(500);
NavigationManager.NavigateTo("/accounts");
}
else
{
errorMessage = "登录失败";
}
}
}
public class UserLoginViewModel
{
[Required(ErrorMessage = "用户名不能为空")]
[StringLength(16, ErrorMessage = "用户名不应超出16个字符长度")]
public string UserName { get; set; }
[Required(ErrorMessage = "密码不能为空")]
public string Password { get; set; }
}
效果大致如下,当然后面还得美化一下。
接下来要通过登录后获得的token,作为全局的变量去请求后端,后端server也可以作为一个统一的全局变量。
通过server加上接口名,加token到header去请求各接口。同时注意token过期的逻辑,并退出登录状态提示再次登录。