Propagate the ModelState errors on dynamic forms

This commit is contained in:
nicolas.dorier 2022-11-25 18:14:33 +09:00
parent 5ff1a59a99
commit 31b25ca169
No known key found for this signature in database
GPG key ID: 6618763EF09186FE
19 changed files with 206 additions and 155 deletions

View file

@ -10,32 +10,55 @@ namespace BTCPayServer.Abstractions.Form;
public class Field public class Field
{ {
public static Field Create(string label, string name, string value, bool required, string helpText, string type = "text")
{
return new Field()
{
Label = label,
Name = name,
Value = value,
OriginalValue = value,
Required = required,
HelpText = helpText,
Type = type
};
}
// The name of the HTML5 node. Should be used as the key for the posted data. // The name of the HTML5 node. Should be used as the key for the posted data.
public string Name; public string Name;
public bool Hidden;
// HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options). // HTML5 compatible type string like "text", "textarea", "email", "password", etc. Each type is a class and may contain more fields (i.e. "select" would have options).
public string Type; public string Type;
public static Field CreateFieldset()
{
return new Field() { Type = "fieldset" };
}
// The value field is what is currently in the DB or what the user entered, but possibly not saved yet due to validation errors. // The value field is what is currently in the DB or what the user entered, but possibly not saved yet due to validation errors.
// If this is the first the user sees the form, then value and original value are the same. Value changes as the user starts interacting with the form. // If this is the first the user sees the form, then value and original value are the same. Value changes as the user starts interacting with the form.
public string Value; public string Value;
public bool Required; public bool Required;
// The translated label of the field.
public string Label;
// The original value is the value that is currently saved in the backend. A "reset" button can be used to revert back to this. Should only be set from the constructor.
public string OriginalValue;
// A useful note shown below the field or via a tooltip / info icon. Should be translated for the user.
public string HelpText;
[JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; } [JsonExtensionData] public IDictionary<string, JToken> AdditionalData { get; set; }
public List<Field> Fields { get; set; } = new(); public List<Field> Fields { get; set; } = new();
public virtual void Validate(ModelStateDictionary modelState) // The field is considered "valid" if there are no validation errors
{ public List<string> ValidationErrors = new List<string>();
if (Required && string.IsNullOrEmpty(Value))
{
modelState.AddModelError(Name, "This field is required");
}
}
public bool IsValid() public virtual bool IsValid()
{ {
ModelStateDictionary modelState = new ModelStateDictionary(); return ValidationErrors.Count == 0 && Fields.All(field => field.IsValid());
Validate(modelState);
return modelState.IsValid;
} }
} }

View file

@ -1,12 +0,0 @@
namespace BTCPayServer.Abstractions.Form;
public class Fieldset : Field
{
public bool Hidden { get; set; }
public string Label { get; set; }
public Fieldset()
{
Type = "fieldset";
}
}

View file

@ -29,7 +29,7 @@ public class Form
// Are all the fields valid in the form? // Are all the fields valid in the form?
public bool IsValid() public bool IsValid()
{ {
return Validate(null); return Fields.Select(f => f.IsValid()).All(o => o);
} }
public Field GetFieldByName(string name) public Field GetFieldByName(string name)
@ -65,16 +65,6 @@ public class Form
return null; return null;
} }
#nullable enable
public bool Validate(ModelStateDictionary? modelState)
{
modelState ??= new ModelStateDictionary();
foreach (var field in Fields)
field.Validate(modelState);
return modelState.IsValid;
}
#nullable restore
public List<string> GetAllNames() public List<string> GetAllNames()
{ {
return GetAllNames(Fields); return GetAllNames(Fields);

View file

@ -1,27 +0,0 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Abstractions.Form;
public class HtmlInputField : Field
{
// The translated label of the field.
public string Label;
// The original value is the value that is currently saved in the backend. A "reset" button can be used to revert back to this. Should only be set from the constructor.
public string OriginalValue;
// A useful note shown below the field or via a tooltip / info icon. Should be translated for the user.
public string HelpText;
public HtmlInputField(string label, string name, string value, bool required, string helpText, string type = "text")
{
Label = label;
Name = name;
Value = value;
OriginalValue = value;
Required = required;
HelpText = helpText;
Type = type;
}
// TODO JSON parsing from string to objects again probably won't work out of the box because of the different field types.
}

View file

@ -10,6 +10,7 @@ using BTCPayServer.Client;
using BTCPayServer.Client.Models; using BTCPayServer.Client.Models;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels; using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest; using BTCPayServer.PaymentRequest;
@ -41,6 +42,8 @@ namespace BTCPayServer.Controllers
private readonly InvoiceRepository _InvoiceRepository; private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
public FormComponentProviders FormProviders { get; }
public UIPaymentRequestController( public UIPaymentRequestController(
UIInvoiceController invoiceController, UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager, UserManager<ApplicationUser> userManager,
@ -49,7 +52,8 @@ namespace BTCPayServer.Controllers
EventAggregator eventAggregator, EventAggregator eventAggregator,
CurrencyNameTable currencies, CurrencyNameTable currencies,
StoreRepository storeRepository, StoreRepository storeRepository,
InvoiceRepository invoiceRepository) InvoiceRepository invoiceRepository,
FormComponentProviders formProviders)
{ {
_InvoiceController = invoiceController; _InvoiceController = invoiceController;
_UserManager = userManager; _UserManager = userManager;
@ -59,6 +63,7 @@ namespace BTCPayServer.Controllers
_Currencies = currencies; _Currencies = currencies;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_InvoiceRepository = invoiceRepository; _InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
} }
[BitpayAPIConstraint(false)] [BitpayAPIConstraint(false)]
@ -204,7 +209,7 @@ namespace BTCPayServer.Controllers
// POST case: Handle form submit // POST case: Handle form submit
var formData = Form.Parse(Forms.UIFormsController.GetFormData(prFormId).Config); var formData = Form.Parse(Forms.UIFormsController.GetFormData(prFormId).Config);
formData.ApplyValuesFromForm(this.Request.Form); formData.ApplyValuesFromForm(this.Request.Form);
if (formData.IsValid()) if (FormProviders.Validate(formData, ModelState))
{ {
prBlob.FormResponse = JObject.FromObject(formData.GetValues()); prBlob.FormResponse = JObject.FromObject(formData.GetValues());
result.SetBlob(prBlob); result.SetBlob(prBlob);

View file

@ -1,20 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms;
public class FormComponentProvider : IFormComponentProvider
{
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
public FormComponentProvider(IEnumerable<IFormComponentProvider> formComponentProviders)
{
_formComponentProviders = formComponentProviders;
}
public string CanHandle(Field field)
{
return _formComponentProviders.Select(formComponentProvider => formComponentProvider.CanHandle(field)).FirstOrDefault(result => !string.IsNullOrEmpty(result));
}
}

View file

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace BTCPayServer.Forms;
public class FormComponentProviders
{
private readonly IEnumerable<IFormComponentProvider> _formComponentProviders;
public Dictionary<string, IFormComponentProvider> TypeToComponentProvider = new Dictionary<string, IFormComponentProvider>();
public FormComponentProviders(IEnumerable<IFormComponentProvider> formComponentProviders)
{
_formComponentProviders = formComponentProviders;
foreach (var prov in _formComponentProviders)
prov.Register(TypeToComponentProvider);
}
public bool Validate(Form form, ModelStateDictionary modelState)
{
foreach (var field in form.Fields)
{
if (TypeToComponentProvider.TryGetValue(field.Type, out var provider))
{
provider.Validate(form, field);
foreach (var err in field.ValidationErrors)
modelState.TryAddModelError(field.Name, err);
}
}
return modelState.IsValid;
}
}

View file

@ -10,7 +10,7 @@ public static class FormDataExtensions
public static void AddForms(this IServiceCollection serviceCollection) public static void AddForms(this IServiceCollection serviceCollection)
{ {
serviceCollection.AddSingleton<FormDataService>(); serviceCollection.AddSingleton<FormDataService>();
serviceCollection.AddSingleton<FormComponentProvider>(); serviceCollection.AddSingleton<FormComponentProviders>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>(); serviceCollection.AddSingleton<IFormComponentProvider, HtmlInputFormProvider>();
serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>(); serviceCollection.AddSingleton<IFormComponentProvider, HtmlFieldsetFormProvider>();
} }

View file

@ -15,21 +15,21 @@ public class FormDataService
public static readonly Form StaticFormEmail = new() public static readonly Form StaticFormEmail = new()
{ {
Fields = new List<Field>() {new HtmlInputField("Enter your email", "buyerEmail", null, true, null)} Fields = new List<Field>() {Field.Create("Enter your email", "buyerEmail", null, true, null, "email")}
}; };
public static readonly Form StaticFormAddress = new() public static readonly Form StaticFormAddress = new()
{ {
Fields = new List<Field>() Fields = new List<Field>()
{ {
new HtmlInputField("Enter your email", "buyerEmail", null, true, null, "email"), Field.Create("Enter your email", "buyerEmail", null, true, null, "email"),
new HtmlInputField("Name", "buyerName", null, true, null), Field.Create("Name", "buyerName", null, true, null),
new HtmlInputField("Address Line 1", "buyerAddress1", null, true, null), Field.Create("Address Line 1", "buyerAddress1", null, true, null),
new HtmlInputField("Address Line 2", "buyerAddress2", null, false, null), Field.Create("Address Line 2", "buyerAddress2", null, false, null),
new HtmlInputField("City", "buyerCity", null, true, null), Field.Create("City", "buyerCity", null, true, null),
new HtmlInputField("Postcode", "buyerZip", null, false, null), Field.Create("Postcode", "buyerZip", null, false, null),
new HtmlInputField("State", "buyerState", null, false, null), Field.Create("State", "buyerState", null, false, null),
new HtmlInputField("Country", "buyerCountry", null, true, null) Field.Create("Country", "buyerCountry", null, true, null)
} }
}; };
} }

View file

@ -1,12 +1,23 @@
using System.Linq; using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Form; using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms; namespace BTCPayServer.Forms;
public class HtmlFieldsetFormProvider: IFormComponentProvider public class HtmlFieldsetFormProvider: IFormComponentProvider
{ {
public string CanHandle(Field field) public string View => "Forms/FieldSetElement";
public void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{
typeToComponentProvider.Add("fieldset", this);
}
public void Validate(Field field)
{
}
public void Validate(Form form, Field field)
{ {
return new[] { "fieldset"}.Contains(field.Type) ? "Forms/FieldSetElement" : null;
} }
} }

View file

@ -1,13 +1,16 @@
using System.Linq; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Abstractions.Form; using BTCPayServer.Abstractions.Form;
using BTCPayServer.Validation;
namespace BTCPayServer.Forms; namespace BTCPayServer.Forms;
public class HtmlInputFormProvider: IFormComponentProvider public class HtmlInputFormProvider: FormComponentProviderBase
{ {
public string CanHandle(Field field) public override void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider)
{ {
return new[] { foreach (var t in new[] {
"text", "text",
"radio", "radio",
"checkbox", "checkbox",
@ -29,6 +32,20 @@ public class HtmlInputFormProvider: IFormComponentProvider
"search", "search",
"url", "url",
"tel", "tel",
"reset"}.Contains(field.Type) ? "Forms/InputElement" : null; "reset"})
typeToComponentProvider.Add(t, this);
}
public override string View => "Forms/InputElement";
public override void Validate(Form form, Field field)
{
if (field.Required)
{
ValidateField<RequiredAttribute>(field);
}
if (field.Type == "email")
{
ValidateField<MailboxAddressAttribute>(field);
}
} }
} }

View file

@ -1,8 +1,26 @@
using BTCPayServer.Abstractions.Form; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Abstractions.Form;
namespace BTCPayServer.Forms; namespace BTCPayServer.Forms;
public interface IFormComponentProvider public interface IFormComponentProvider
{ {
public string CanHandle(Field field); string View { get; }
void Validate(Form form, Field field);
void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
}
public abstract class FormComponentProviderBase : IFormComponentProvider
{
public abstract string View { get; }
public abstract void Register(Dictionary<string, IFormComponentProvider> typeToComponentProvider);
public abstract void Validate(Form form, Field field);
public void ValidateField<T>(Field field) where T : ValidationAttribute, new()
{
var result = new T().GetValidationResult(field.Value, new ValidationContext(field) { DisplayName = field.Label, MemberName = field.Name });
if (result != null)
field.ValidationErrors.Add(result.ErrorMessage);
}
} }

View file

@ -26,6 +26,12 @@ namespace BTCPayServer.Forms;
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public class UIFormsController : Controller public class UIFormsController : Controller
{ {
public FormComponentProviders FormProviders { get; }
public UIFormsController(FormComponentProviders formProviders)
{
FormProviders = formProviders;
}
[AllowAnonymous] [AllowAnonymous]
[HttpGet("~/forms/{formId}")] [HttpGet("~/forms/{formId}")]
[HttpPost("~/forms")] [HttpPost("~/forms")]
@ -39,24 +45,32 @@ public class UIFormsController : Controller
: Redirect(redirectUrl); : Redirect(redirectUrl);
} }
return View("View", new FormViewModel() { FormData = formData, RedirectUrl = redirectUrl }); return GetFormView(formData, redirectUrl);
} }
ViewResult GetFormView(FormData formData, string? redirectUrl)
{
return View("View", new FormViewModel() { FormData = formData, RedirectUrl = redirectUrl });
}
[AllowAnonymous] [AllowAnonymous]
[HttpPost("~/forms/{formId}")] [HttpPost("~/forms/{formId}")]
public IActionResult SubmitForm( public IActionResult SubmitForm(
string formId, string formId,
string? redirectUrl, string? redirectUrl,
string? command,
[FromServices] StoreRepository storeRepository, [FromServices] StoreRepository storeRepository,
[FromServices] UIInvoiceController invoiceController) [FromServices] UIInvoiceController invoiceController)
{ {
var formData = GetFormData(formId); var formData = GetFormData(formId);
if (formData?.Config is null) if (formData?.Config is null)
return NotFound(); return NotFound();
if (command is not "Submit")
return GetFormView(formData, redirectUrl);
var conf = Form.Parse(formData.Config); var conf = Form.Parse(formData.Config);
conf.ApplyValuesFromForm(Request.Form); conf.ApplyValuesFromForm(Request.Form);
if (!conf.Validate(ModelState)) if (!FormProviders.Validate(conf, ModelState))
return View("View", new FormViewModel() { FormData = formData, RedirectUrl = redirectUrl }); return GetFormView(formData, redirectUrl);
var form = new MultiValueDictionary<string, string>(); var form = new MultiValueDictionary<string, string>();
foreach (var kv in Request.Form) foreach (var kv in Request.Form)

View file

@ -15,6 +15,7 @@ using BTCPayServer.Client;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Filters; using BTCPayServer.Filters;
using BTCPayServer.Forms;
using BTCPayServer.ModelBinders; using BTCPayServer.ModelBinders;
using BTCPayServer.Models; using BTCPayServer.Models;
using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Plugins.PointOfSale.Models;
@ -40,12 +41,14 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
AppService appService, AppService appService,
CurrencyNameTable currencies, CurrencyNameTable currencies,
StoreRepository storeRepository, StoreRepository storeRepository,
UIInvoiceController invoiceController) UIInvoiceController invoiceController,
FormComponentProviders formProviders)
{ {
_currencies = currencies; _currencies = currencies;
_appService = appService; _appService = appService;
_storeRepository = storeRepository; _storeRepository = storeRepository;
_invoiceController = invoiceController; _invoiceController = invoiceController;
FormProviders = formProviders;
} }
private readonly CurrencyNameTable _currencies; private readonly CurrencyNameTable _currencies;
@ -53,6 +56,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
private readonly AppService _appService; private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController; private readonly UIInvoiceController _invoiceController;
public FormComponentProviders FormProviders { get; }
[HttpGet("/")] [HttpGet("/")]
[HttpGet("/apps/{appId}/pos/{viewType?}")] [HttpGet("/apps/{appId}/pos/{viewType?}")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)] [XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
@ -232,7 +237,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var formData = Form.Parse(Forms.UIFormsController.GetFormData(posFormId).Config); var formData = Form.Parse(Forms.UIFormsController.GetFormData(posFormId).Config);
formData.ApplyValuesFromForm(this.Request.Form); formData.ApplyValuesFromForm(this.Request.Form);
if (formData.IsValid()) if (FormProviders.Validate(formData, ModelState))
{ {
formResponse = JObject.FromObject(formData.GetValues()); formResponse = JObject.FromObject(formData.GetValues());
break; break;

View file

@ -1,27 +1,19 @@
@using BTCPayServer.Abstractions.Form @using BTCPayServer.Abstractions.Form
@using BTCPayServer.Forms @using BTCPayServer.Forms
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using Newtonsoft.Json.Linq @using Newtonsoft.Json.Linq
@inject FormComponentProvider FormComponentProvider @inject FormComponentProviders FormComponentProviders
@model BTCPayServer.Abstractions.Form.Field @model BTCPayServer.Abstractions.Form.Field
@{ @if (!Model.Hidden)
if (Model is not Fieldset fieldset)
{
fieldset = JObject.FromObject(Model).ToObject<Fieldset>();
}
}
@if (!fieldset.Hidden)
{ {
<fieldset> <fieldset>
<legend class="h3 mt-4 mb-3">@fieldset.Label</legend> <legend class="h3 mt-4 mb-3">@Model.Label</legend>
@foreach (var field in fieldset.Fields) @foreach (var field in Model.Fields)
{ {
var partial = FormComponentProvider.CanHandle(field); if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
if (string.IsNullOrEmpty(partial))
{ {
continue; <partial name="@partial.View" for="@field"></partial>
} }
<partial name="@partial" for="@field"></partial>
} }
</fieldset> </fieldset>
} }

View file

@ -1,31 +1,34 @@
@using BTCPayServer.Abstractions.Form @using BTCPayServer.Abstractions.Form
@using Newtonsoft.Json.Linq @using Newtonsoft.Json.Linq
@model BTCPayServer.Abstractions.Form.Field @model BTCPayServer.Abstractions.Form.Field
@{ @{
if (Model is not HtmlInputField field) var isInvalid = this.ViewContext.ModelState[Model.Name]?.ValidationState is Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid;
{ var error = isInvalid ? this.ViewContext.ModelState[Model.Name].Errors[0].ErrorMessage : null;
field = JObject.FromObject(Model).ToObject<HtmlInputField>();
}
} }
<div class="form-group"> <div class="form-group">
@if (field.Required) @if (Model.Required)
{ {
<label class="form-label" for="@field.Name" data-required> <label class="form-label" for="@Model.Name" data-required>
@field.Label @Model.Label
</label> </label>
} }
else else
{ {
<label class="form-label" for="@field.Name"> <label class="form-label" for="@Model.Name">
@field.Label @Model.Label
</label> </label>
} }
<input class="form-control @(field.IsValid() ? "" : "is-invalid")" id="@field.Name" type="@field.Type" required="@field.Required" name="@field.Name" value="@field.Value" aria-describedby="@("HelpText" + field.Name)"/> <input class="form-control @(Model.IsValid() ? "" : "is-invalid")" id="@Model.Name" type="@Model.Type" required="@Model.Required" name="@Model.Name" value="@Model.Value" aria-describedby="@("HelpText" + Model.Name)"/>
@if (!string.IsNullOrEmpty(field.HelpText)) @if(isInvalid)
{ {
<small id="@("HelpText" + field.Name)" class="form-text text-muted"> <span class="text-danger">@error</span>
@field.HelpText }
@if (!string.IsNullOrEmpty(Model.HelpText))
{
<small id="@("HelpText" + Model.Name)" class="form-text text-muted">
@Model.HelpText
</small> </small>
} }

View file

@ -1,14 +1,12 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers @using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Forms @using BTCPayServer.Forms
@model BTCPayServer.Abstractions.Form.Form @model BTCPayServer.Abstractions.Form.Form
@inject FormComponentProvider FormComponentProvider @inject FormComponentProviders FormComponentProviders
@foreach (var field in Model.Fields) @foreach (var field in Model.Fields)
{ {
var partial = FormComponentProvider.CanHandle(field); if (FormComponentProviders.TypeToComponentProvider.TryGetValue(field.Type, out var partial))
if (string.IsNullOrEmpty(partial))
{ {
continue; <partial name="@partial.View" for="@field"></partial>
} }
<partial name="@partial" for="@field"></partial>
} }

View file

@ -33,7 +33,7 @@
<input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl"/> <input type="hidden" asp-for="RedirectUrl" value="@Model.RedirectUrl"/>
} }
<partial name="_Form" model="@Model.Form"/> <partial name="_Form" model="@Model.Form"/>
<input type="submit" class="btn btn-primary" value="Submit"/> <input type="submit" class="btn btn-primary" name="command" value="Submit"/>
</form> </form>
</div> </div>
</div> </div>

View file

@ -34,16 +34,16 @@ public class FakeCustodian : ICustodian
var fakeConfig = ParseConfig(config); var fakeConfig = ParseConfig(config);
var form = new Form(); var form = new Form();
var fieldset = new Fieldset(); var fieldset = Field.CreateFieldset();
// Maybe a decimal type field would be better? // Maybe a decimal type field would be better?
var fakeBTCBalance = new HtmlInputField("BTC Balance", "BTCBalance", fakeConfig?.BTCBalance.ToString(), true, var fakeBTCBalance = Field.Create("BTC Balance", "BTCBalance", fakeConfig?.BTCBalance.ToString(), true,
"Enter the amount of BTC you want to have."); "Enter the amount of BTC you want to have.");
var fakeLTCBalance = new HtmlInputField("LTC Balance", "LTCBalance", fakeConfig?.LTCBalance.ToString(), true, var fakeLTCBalance = Field.Create("LTC Balance", "LTCBalance", fakeConfig?.LTCBalance.ToString(), true,
"Enter the amount of LTC you want to have."); "Enter the amount of LTC you want to have.");
var fakeEURBalance = new HtmlInputField("EUR Balance", "EURBalance", fakeConfig?.EURBalance.ToString(), true, var fakeEURBalance = Field.Create("EUR Balance", "EURBalance", fakeConfig?.EURBalance.ToString(), true,
"Enter the amount of EUR you want to have."); "Enter the amount of EUR you want to have.");
var fakeUSDBalance = new HtmlInputField("USD Balance", "USDBalance", fakeConfig?.USDBalance.ToString(), true, var fakeUSDBalance = Field.Create("USD Balance", "USDBalance", fakeConfig?.USDBalance.ToString(), true,
"Enter the amount of USD you want to have."); "Enter the amount of USD you want to have.");
fieldset.Label = "Your fake balances"; fieldset.Label = "Your fake balances";