Hello
In standard DotVVM application after logging in, the browser will not offer to save the login and password. This is because the login is processed by postback, and for the browser to offer to save the password, the login needs to be processed by form submit.
If you have same problem, here is my solution of the login dialog to make saving password working.
In login page I have second form like this:
<form id="submitForm" method="post" role="form" action="{resource: RedirectUrl}"></form>
Which after authenticate does the actual submit.
And form submit calls this javascript, which catches the setting of the Submit=true property in the viewmodel.
dotvvm.events.init.subscribe(function () {
dotvvm.events.afterPostback.subscribe(function (args) {
if (!args.wasInterrupted && args.serverResponseObject) {
if (args.serverResponseObject.action === "successfulCommand") {
var submit = args.serverResponseObject.viewModel.Submit;
if (submit) {
document.getElementById('submitForm').submit();
}
}
}
});
});
Here is the full code
Login.dothtml:
@viewModel Web.ViewModels.LoginViewModel, Web
@masterPage Views/SiteMaster.dotmaster
<dot:Content ContentPlaceHolderID="MainContent">
<div class="container py-5 h-100">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col-12 col-sm-11 col-md-8 col-lg-6 col-xl-5">
<div class="card-body p-5 text-center">
<h1 class="mb-4">Sign in</h1>
<div class="mb-4">
<div Visible="{value: LoginErrorMessage != null}" style="display: none;">
<div class="alert d-flex align-items-center alert-danger alert-dismissible" role="alert">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="#832002" class="me-3" viewBox="0 0 48 48">
<path d="m22.12 3.087-1.2.161c-4.286.569-8.584 2.738-11.798 5.955-3.295 3.297-5.326 7.451-5.964 12.2-.154 1.151-.177 3.639-.043 4.837a21.054 21.054 0 0 0 16.573 18.311c2.669.57 5.955.57 8.624 0a21.106 21.106 0 0 0 13.628-9.64c3.048-4.964 3.853-11.102 2.206-16.831-2.02-7.031-7.902-12.68-15.026-14.431-1.832-.45-2.611-.542-4.8-.57-1.078-.013-2.068-.01-2.2.008m5.04 2.185c4.09.729 7.62 2.587 10.443 5.499 2.744 2.83 4.423 6.134 5.133 10.103.246 1.374.266 4.68.036 6.046-.341 2.035-.883 3.724-1.77 5.52-2.429 4.92-6.912 8.556-12.282 9.96-2.988.781-6.452.781-9.44 0-5.343-1.398-9.76-4.961-12.244-9.88-.865-1.711-1.409-3.367-1.772-5.394-.158-.878-.181-1.276-.181-3.126 0-1.85.023-2.248.181-3.126.363-2.027.907-3.683 1.772-5.394C9.804 10 15.082 6.144 21.04 5.249c1.3-.195 1.287-.194 3.28-.17 1.492.018 2.078.058 2.84.193m-3.841 8.033a1.012 1.012 0 0 0-.316.62c-.028.195-.04 3.057-.027 6.359.022 5.577.034 6.018.165 6.218.423.646 1.295.646 1.718 0 .131-.2.143-.641.165-6.218.013-3.302.001-6.164-.027-6.359a1.012 1.012 0 0 0-.316-.62c-.232-.232-.317-.265-.681-.265-.364 0-.449.033-.681.265m.008 16.821a2.014 2.014 0 0 0-1.065.897c-.189.321-.222.465-.222.977s.033.656.222.977c.677 1.152 2.206 1.359 3.14.425.359-.359.598-.919.598-1.402 0-1.347-1.411-2.337-2.673-1.874" />
</svg>
<div>
<dot:HtmlLiteral Html="{value: LoginErrorMessage}" />
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
</div>
<form id="loginForm" Validator.InvalidCssClass="is-invalid">
<div class="form-outline mb-4">
<dot:TextBox id="userName" Type="Normal" Text="{value: UserName}" name="UserName" placeholder="UserName" class="form-control form-control-lg" autocomplete="off" Validator.Value="{value: UserName}" Validator.SetToolTipText="true" Changed="{command: ControlChanged()}" PostBack.ConcurrencyQueue="HideProgress" />
<div class="invalid-feedback">
<dot:Validator Value="{value: UserName}" ShowErrorMessageText="true" />
</div>
</div>
<div class="form-outline mb-3">
<dot:TextBox id="password" Type="Password" Text="{value: Password}" name="Password" placeholder="Password" class="form-control form-control-lg" autocomplete="off" Validator.Value="{value: Password}" Validator.SetToolTipText="true" Changed="{command: ControlChanged()}" PostBack.ConcurrencyQueue="HideProgress" />
<div class="invalid-feedback">
<dot:Validator Value="{value: Password}" ShowErrorMessageText="true" />
</div>
</div>
<div class="form-check d-flex justify-content-start mb-4 no-padding-left">
<dot:CheckBox class="checkbox" Text="Keep me signed in" Checked="{value: RememberMe}" Changed="{command: ControlChanged()}" PostBack.ConcurrencyQueue="HideProgress" Validation.Enabled="false" />
</div>
<div class="pull-right">
<dot:Button id="loginButton" ButtonTagName="button" IsSubmitButton="true" class="btn primary" Click="{command: SignIn()}" PostBack.Concurrency="Deny">
Sign in
</dot:Button>
</div>
<div class="clearfix"></div>
</form>
<form id="submitForm" method="post" role="form" action="{resource: RedirectUrl}"></form>
</div>
</div>
</div>
</div>
</dot:Content>
<dot:Content ContentPlaceHolderID="ScriptsContent">
<dot:InlineScript>
window.onload = function () {
$('#userName').focus();
$('#userName').select();
}
dotvvm.events.init.subscribe(function () {
dotvvm.events.afterPostback.subscribe(function (args) {
if (!args.wasInterrupted && args.serverResponseObject) {
if (args.serverResponseObject.action === "successfulCommand") {
var submit = args.serverResponseObject.viewModel.Submit;
if (submit) {
document.getElementById('submitForm').submit();
}
}
}
});
});
</dot:InlineScript>
</dot:Content>
LoginViewModel.cs:
using System.ComponentModel.DataAnnotations;
namespace Web.ViewModels;
public class LoginViewModel : SiteMasterViewModel
{
private readonly LoginService LoginService;
[Required(ErrorMessage = "E-mail is required.")]
public string UserName { get; set; } = null!;
public string? Password { get; set; }
public bool RememberMe { get; set; }
public string? LoginErrorMessage { get; set; }
public string? RedirectUrl { get; set; }
public bool Submit { get; set; }
public LoginViewModel(LoginService loginService)
{
this.LoginService = loginService;
}
public override Task Load()
{
if (!this.Context.IsPostBack)
{
string? originalUrl = this.Context.HttpContext.Request.Query["originalUrl"];
if (originalUrl != null && !originalUrl.StartsWith('/'))
{
originalUrl = null;
}
if (originalUrl != null)
{
this.RedirectUrl = this.Context.GetAspNetCoreContext().Request.GetRelativeUrl("~" + originalUrl);
}
else
{
this.RedirectUrl = this.Context.GetAspNetCoreContext().Request.GetRelativeUrl("~/"); //Default route
}
if (this.Context.HttpContext.User?.Identity?.IsAuthenticated == true)
{
this.Context.RedirectToUrl(this.RedirectUrl);
}
}
return Task.CompletedTask;
}
public async Task SignIn()
{
this.LoginErrorMessage = null;
this.Submit = false;
try
{
await this.LoginService.Authenticate(this.UserName, this.Password ?? string.Empty, this.RememberMe);
//This is caught by javascript on the page
this.Submit = true;
}
catch (AuthenticationException ex)
{
this.LoginErrorMessage = ex.Message;
}
catch (UserLockedOutException ex)
{
this.LoginErrorMessage = ex.Message;
}
}
public void ControlChanged() { }
}
and originalUrl is set in CookieAuthenticationOptions OnRedirectToLogin event like this:
options.Events.OnRedirectToLogin = redirectContext =>
{
string? originalReturnUrl = string.IsNullOrEmpty(redirectContext.RedirectUri) ? null : System.Web.HttpUtility.ParseQueryString(new Uri(redirectContext.RedirectUri).Query)["ReturnUrl"];
string? originalUrl = string.IsNullOrEmpty(originalReturnUrl) ? null : redirectContext.Request.GetAppRelativeUrl(originalReturnUrl)[1..];
if (string.IsNullOrEmpty(originalUrl))
{
if (!(string.IsNullOrEmpty(redirectContext.Request.Path) && !redirectContext.Request.QueryString.HasValue) &&
!(string.Equals(redirectContext.Request.Path, "/", StringComparison.Ordinal) && !redirectContext.Request.QueryString.HasValue) &&
!string.Equals(redirectContext.Request.Path, "/not-found", StringComparison.OrdinalIgnoreCase) &&
!redirectContext.Request.Path.ToString().StartsWith("/error", StringComparison.OrdinalIgnoreCase))
{
originalUrl = redirectContext.Request.Path + redirectContext.Request.QueryString.Value;
}
}
if (originalUrl == "/")
{
originalUrl = null;
}
var query = QueryString.Empty;
if (originalUrl != null)
{
query = query.Add("originalUrl", originalUrl);
}
string redirectUri = redirectContext.Request.GetRelativeUrl(LoginUrl) + query.Value;
redirectContext.HttpContext.Response.Redirect(redirectUri);
return Task.CompletedTask;
};
Note: I here use originalUrl querystring instead of standard returnUrl because this solution doesnβt work with returnUrl. returnUrl is somehow used internally by DotVVM and the postback is not called correctly after logging in.