Validering
Hvordan legge til logikk for å validere skjemadata?
Introduksjon
Valideringer sørger for at brukerens input er gyldig med tanke på datamodellen, i tillegg til alle egendefinerte regler som settes opp for applikasjonen. Valideringer kan kjøres enten på klient (dvs. browseren) eller serversiden.
Man kan også sette opp validering til å kjøre ved sidebytte.
Klientside-validering
Dette er validering som kjøres i browseren, FØR data er sendt til server for lagring. Dette gjør det mulig å gi raske tilbakemeldinger til sluttbruker underveis i utfylling.
Klientside-validering baserer seg på datamodellen som hører til skjemaet, og bruker denne til å bestemme hva som er gyldig input i et felt. Helt konkret brukes JSON Schema utgaven av datamodellen for valideringen. Denne genereres automatisk når man laster opp XSD. Det går an å gjøre endringer i JSON schema direkte for å tilpasse valideringen ved behov.
Merk at dersom man gjør tilpasninger i JSON schema manuelt, for å så oppdatere XSD og laste inn på nytt, vil nytt JSON schema også genereres, og alle manuelle tilpasninger må gjøres på nytt. Derfor er det anbefalt å gjøre endringer i XSD og/eller datamodelleringsverktøyet for at disse endringene skal reflekteres i JSON schema.
Et eksempel på hvordan et felt kan defineres i JSON schema datamodellen er:
"someField": {
"type": "string",
"maxLength": "4"
}
Input i dette feltet vil valideres mot begrensningene som er satt opp, og en feilmelding vil vises dersom disse ikke møtes - i dette tilfellet, dersom input er en tekst med lengde mer enn 4 karakterer.
Standard feilmeldinger
Det er satt opp standard feilmeldinger for alle valideringene som gjøres på klientsiden. Se oversikten under.
Regel | Feilmelding bokmål | Feilmelding nynorsk | Feilmelding engelsk |
---|---|---|---|
minimum | ‘Minste gyldig verdi er {0}’ | ‘Minste gyldig verdi er {0}’ | ‘Minimum valid value is {0}’ |
maximum | ‘Største gyldig verdi er {0}’ | ‘Største gyldig verdi er {0}’ | ‘Maximum valid value is {0}’ |
minLength | ‘Bruk {0} eller flere tegn’ | ‘Bruk {0} eller flere tegn’ | ‘Use {0} or more characters’ |
maxLength | ‘Bruk {0} eller færre tegn’ | ‘Bruk {0} eller færre tegn’ | ‘Use {0} or fewer characters’ |
length | ‘Antall tillatte tegn er {0}’ | ‘Antall tillatte tegn er {0}’ | ‘Number of characters allowed is {0}’ |
pattern | ‘Feil format eller verdi’ | ‘Feil format eller verdi’ | ‘Wrong format or value’ |
required | ‘Du må fylle ut {0}’ | ‘Du må fylle ut {0}’ | ‘You have to fill out {0}’ |
enum | ‘Kun verdiene {0} er tillatt’ | ‘Kun verdiene {0} er tillatt’ | ‘Only the values {0} are permitted’ |
Spesielt om standard feilmelding for påkrevde felter
For en smidigere brukeropplevelse vises ikke feilmeldinger for manglende utfylling av påkrevde felter under utfylling av et skjema, med mindre validering trigges på et enkeltfelt, ved lagring av en rad i en repeterende gruppe eller ved navigering til en annen side.
Feilmeldingen for påkrevde felter er “Du må fylle ut {0}”. Her blir {0}
erstattet med det feltet som feilmeldingen gjelder for.
Dette gjøres på følgende måte:
- Bruker feltets
shortName
tekst. Dette er en ny tekst som kan settes opp pr. komponent på samme måte som ledetekst (title
) settes i dag. Denne teksten brukes pr nå KUN i forbindelse med feilmeldingen for påkrevde felter. - Om
shortName
ikke er definert brukes feltetstitle
tekst (det som er definert som ledetekst for feltet), og teksten vil bli forsøkt gjort om til en tekst med liten forbokstav (med mindre teksten ser ut som en forkortelse). - I noen spesialtilfeller (Adresse-komponenten) der det er flere felter i ett brukes de standard-tekstene som er definert for feltene i komponenten.
Eksempel: Felt med kun title
{
"id": "fornavn",
"type": "Input",
"textResourceBindings": {
"title": "tekst-fornavn"
},
... //osv
}
Og tekster i ressurs-fil:
...
{
"id": "tekst-fornavn",
"value": "Fornavn"
}
Da vil valideringmeldingen bli "Du må fylle ut Fornavn"
.
Eksempel: Felt med shortName
Dersom feltets ledetekst er lang eller ikke egner seg til bruk i valideringsmeldingen, kan man legge til en shortName
tekst som brukes i stedet.
Merk at dette kun gjelder for denne spesifikke valideringsmeldingen - shortName
teksten er ikke i bruk ellers i løsningen pr nå.
{
"id": "fornavn",
"type": "Input",
"textResourceBindings": {
"title": "tekst-fornavn",
"shortName": "fornavn-kort"
},
... //osv
}
Og tekster i ressurs-fil:
...
{
"id": "tekst-fornavn",
"value": "Her kan du skrive ditt fornavn",
},
{
"id": "fornavn-kort",
"value": "fornavnet ditt",
}
Da vil valideringmeldingen bli "Du må fylle ut fornavnet ditt"
.
Erstatte feilmelding for påkrevde felter helt
Hvis du ønsker å erstatte standardfeilmeldingen for obligatoriske felt fullstendig, kan du gjøre dette ved å legge til
tekstnøkkelen requiredValidation
i komponentens textResourceBindings
-objekt. Dette vil erstatte standardfeilmeldingen
for obligatoriske felt. Teksten kan være en tekstnøkkel for en tekst som er definert i ressursfilene
for flerspråklig støtte.
{
"id": "firstName",
"type": "Input",
"textResourceBindings": {
"title": "text-firstName",
"requiredValidation": "myCustomRequiredValidation"
},
...
}
Egendefinerte feilmeldinger
Det er mulig å definere egne feilmeldinger som skal vises når et felt får valideringsfeil. Dette gjøres ved å legge på en parameter errorMessage
der
hvor feltet er definert i JSON schema. JSON schema filen ligger i mappen App/models
og følger navnestandard *.schema.json
.
F.eks., man kan utvide eksempelet over:
"someField": {
"type": "string",
"maxLength": "4",
"errorMessage": "myCustomError"
}
Man kan skrive ønsket tekst direkte inn her, eller bruke en tekstnøkkel for en tekst definert i ressursfilene for språkstøtte.
Legg merke til at om man har en referanse til en definisjon så må feilmeldingen ligge på property
-feltet, og ikke på referansen/definisjonen.
Eksempel:
{
"properties": {
"person": {
"$ref" : "#/definitions/personDefinition",
"errorMessage": "myCustomError",
}
},
"definitions": {
"personDefinition" : {
"age": {
"type": "number"
},
...
}
}
Serverside-validering
Serverside-validering kan deles opp i to kategorier:
- Valideringer mot datamodell - Disse kjører automatisk når brukeren prøver å sende inn skjemadata.
- Egendefinerte valideringer - Disse skrives av applikasjonsutvikleren, og kjører når brukeren prøver å sende inn skjemadata eller flytte prosessen til et nytt steg.
Hvordan legge til egendefinert validering
Egendefinerte validering kan igjen deles opp i to kategorier; task-validering og data-validering.
- Task-validering vil kjøres hver gang validering trigges enten manuelt fra applikasjonen eller når man prøver å flytte seg framover i prosessen.
- Data-validering vil kjøre dersom man står på et steg som har definerte dataelementer knyttet til seg.
Valideringer skrives i C# og avhengig av hvilken versjon av applikasjonsmalen og Nuget pakkene du er på, så vil implementeringen variere litt. I tidligere versjon så er det en pre-definert fil med metoder du kan legge inn logikken, mens fra versjon 7 og fremover så implementerer du et grensesnitt i den klassen du selv vil. Grensesnittet er tilfeldigvis likt den pre-definerte filen. Eksemplene som refererer til metoder vil derfor være de samme for alle versjoner.
I versjon 7 har vi endret måten preutfylling med egendefinert kode gjøres på. Vi benytter nå dependency injection i stedet for overstyring av metoder. Hvis du tidligere plasserte koden din i _ValidationHandler og ValidateTask metodene in ValidationHandler.cs klassen så vil du erfare at det er mer eller mindre det samme som nå gjøres.
- Opprett en klasse som implementerer
IInstanceValidator
grensesnittet som ligger iAltinn.App.Core.Features.Validation
navnerommet.
Du kan navngi og plassere filene i den mappestrukturen du selv ønsker i prosjektet ditt. Men vi anbefaler at du benytter meningsfulle navnerom som i et hvilket som helst annet .Net prosjekt. - Registrer din implementering i Program.cs klassenDette sørger for at din kode er kjent for applikasjonen og at koden blir kjørt når den skal.
services.AddTransient<IInstanceValidator, InstanceValidator>();
ValidationHandler.cs
-filen i applikasjonsmalen.
Filen kan aksesseres og endres i Altinn Studio via logikkmenyen, ved å velge Rediger valideringer,
eller direkte i applikasjonsrepoet der ligger filen i logic/Validation
-mappen.Fra dette punktet og videre skal eksemplene være de samme for alle versjoner :)
Endringer gjøres i ValidateData
og ValidateTask
-metodene.
Førstnevnte får inn et dataobjekt og sistnevnte får inn instansen og taskId.
For å legge til en valideringsfeil brukes AddModelError
-metoden til validationResults
object som sendes med i begge metodene.
Et eksempel på en enkel data-validering som sjekker at feltet FirstName ikke inneholder verdien 1337, når rotelementet til modellen er Skjema
er vist nedenfor:
public void ValidateData(object data, ModelStateDictionary validationResults)
{
if (data.GetType() == typeof(Skjema))
{
// Cast instance data to model type
Skjema model = (Skjema)data;
// Get value to test - FirstName
string firstName = Skjema?.Person?.FirstName;
// Check if FirstName exists, and contains the value "1337"
if (firstName != null && firstName.Contains("1337"))
{
// Add validation error, with error message and list
// of affected fields (in this case Person.FirstName)
validationResults.AddModelError(
"Person.FirstName",
"Error: First name cannot contain the value '1337'."
);
}
}
}
Se kommentarer i koden over for en forklaring på hva de ulike delene gjør.
I det andre parameteret til metoden AddModelError
, der det står “Error: First name cannot contain the value ‘1337’”, kan man bruke en tekstnøkkel for en tekst definert i ressursfilene for språkstøtte.
Et eksempel på en enkel task-validering som sjekker hvor lang tid brukeren har brukt på Task_1 og returnerer en feil dersom det har tatt lenger enn 3 dager.
public async Task ValidateTask(Instance instance, string taskId, ModelStateDictionary validationResults)
{
if (taskId.Equals("Task_1"))
{
DateTime deadline = ((DateTime)instance.Created).AddDays(3);
if (DateTime.UtcNow < deadline)
{
validationResults.AddModelError("Task_1", $"Ferdigstilling av Task_1 har tatt for lang tid. Vennligst start på nytt.");
}
}
}
Enkeltfeltvalidering
Enkeltfeltvalidering vises umiddelbart når brukeren har fylt ut et felt.
Ved å sette showValidations
-egenskapen på en komponent vil valideringsfeil gjøres synlig umiddelbart når de oppstår.
{
"id": "some-input-field",
"type": "Input",
"textResourceBindings": {...},
"dataModelBindings": {...},
"showValidations": ["AllExceptRequired"]
}
Hvor showValidations
inneholder et sett med validerings-typer som skal sjekkes; dette kan være én eller flere av:
Schema
Component
Expression
CustomBackend
Required
AllExceptRequired
All
NB: "showValidations": ["AllExceptRequired"]
brukes som standard dersom egenskapen ikke er satt.
For å unngå å vise noen valideringer umiddelbart kan showValidations
settes til en tom liste []
.
Merk at i versjon 3 av app frontend, kjøres JSON schema og komponent-spesifikk validering automatisk som standard, å legge til en validerings-trigger fører til at custom backend validering kjøres i tillegg.
{
"id": "some-input-field",
"type": "Input",
"textResourceBindings": {...},
"dataModelBindings": {...},
"triggers": ["validation"]
}
Konfigurasjonen overfor vil resultere i at din egendefinerte validering i ValidationHandler.cs
vil trigges hver gang feltet oppdaterer seg. Dersom du har behov for å vite hvilket
felt som trigget valideringen er denne tilgjengelig i http-konteksten som en header på requesten ved navn ValidationTriggerField.
Et eksempel på en egendefinert validering der headerverdien hentes ut er vist nedenfor.
public async Task ValidateData(object data, ModelStateDictionary validationResults)
{
_httpContextAccessor.HttpContext
.Request.Headers
.TryGetValue("ValidationTriggerField", out StringValues triggerValues);
string triggerField = triggerValues.FirstOrDefault(string.Empty);
if (triggerField.Equals("kommune"))
{
// Cast instance data to model type
flyttemelding model = (flyttemelding)data;
// Get value to test - Kommune
string kommune = model.kommune;
if (!kommune.Equals("Oslo"))
{
validationResults.AddModelError(triggerField, "Dette er ikke en gyldig kommune.");
}
}
await Task.CompletedTask;
}
OBS Merk at validering av enkeltfelter bør implementeres slik at det kjører både på trigger og under generell validering. Eksempelet som omhandler flere komplekse valideringer viser hvordan dette kan implementeres.
Det er gjort flere ting for å få denne kodesnutten til å kjøre
- I ValidationHandler.cs inkluderes
using Microsoft.Extensions.Primitives;
øverst i filen for å kunne ta i brukStringValues
. - I App.cs inkluderes
using Microsoft.AspNetCore.Http;
øverst i filen for å kunne ta i brukIHttpContextAccessor
. - I App.cs dependency injectes
IHttpContextAccessor
i konstruktøren og sendes med videre til ValidationHandler.
public App(
IAppResources appResourcesService,
ILogger<App> logger,
IData dataService,
IProcess processService,
IPDF pdfService,
IProfile profileService,
IRegister registerService,
IPrefill prefillService,
IHttpContextAccessor httpContextAccessor // <--- Add this line
) : base(appResourcesService, logger, dataService, processService, pdfService, prefillService)
{
_logger = logger;
_validationHandler = new ValidationHandler(httpContextAccessor); // <--- Include the new property here
_calculationHandler = new CalculationHandler();
_instantiationHandler = new InstantiationHandler(profileService, registerService);
}
Dersom man har flere komplekse valideringer som er tidkrevende er det anbefalt å implementere flere private metoder for validering av disse og bruke ValidationTriggerField til å avgjøre hvilken private metode som skal kjøres. Man kan bl.a. bruke en switch statement for å oppnå dette.
public async Task ValidateData(object data, ModelStateDictionary validationResults)
{
if (data is flyttemelding model)
{
_httpContextAccessor.HttpContext
.Request.Headers
.TryGetValue("ValidationTriggerField", out StringValues triggerValues);
string triggerField = triggerValues.FirstOrDefault(string.Empty);
switch (triggerField)
{
case "kommune":
ValidateKommune(model, validationResults);
break;
case "boaddresse":
ValidateBoAdresse(model, validationResults);
break;
default:
ValidateKommune(model, validationResults);
ValidateBoAdresse(model, validationResults);
break;
}
}
}
private void ValidateKommune(flyttemelding model, ModelStateDictionary validationResults)
{
if (model.kommune != null && !model.kommune.Equals("Oslo"))
{
validationResults.AddModelError(
nameof(model.kommune),
"Dette er ikke en gyldig kommune.");
}
}
private void ValidateBoAdresse(flyttemelding model, ModelStateDictionary validationResults)
{
if (model.boaddresse != null && model.boaddresse.Length > 150)
{
validationResults.AddModelError(
nameof(model.boaddresse),
"Boadresse kan ikke være lengere enn 150 tegn.");
}
}
Spesifisere at valideringsfeil er fikset
Når validering trigges av et enkelt felt, så vil alle tidligere valideringer på dette feltet fjernes i påvente av svar fra den siste valideringen. Dersom et felt trigger validering som oppdaterer/legger til feilmelding på flere felter på en gang, vil ikke disse fjernes selv om det ikke lenger er noen feil i disse feltene. Dette er fordi man ikke har noen måte å vite hvilke felter som ev. er validert ifm en enkeltfeltvalidering.
F.eks., dersom man har 2 felter: fornavn og etternavn. Begge felter trigger enkeltfeltvalidering, og dersom begge feltene har verdi så validerer man at fullt navn ikke kan være lengre enn 50 tegn. Feilmelding settes da på begge feltene. Dersom man retter opp i dette ved å endre fornavn, vil feilmeldingen fra fornavn-feltet forsvinne, men feilmeldingen som vises på etternavn-feltet vises fortsatt selv om valideringen ikke setter noen feilmeldinger på feltene.
private void ValidateFullName(Datamodell model, ModelStateDictionary validationResults)
{
if (!string.isNullOrEmpty(model.fornavn) && !string.isNullOrEmpty(model.etternavn)
&& model.fornavn.Length + model.etternavn.Length > 50)
{
validationResults.addModelError(nameof(model.fornavn),
"Fullt navn kan ikke være lengre enn 50 tegn.");
validationResults.addModelError(nameof(model.etternavn),
"Fullt navn kan ikke være lengre enn 50 tegn.");
}
}
For å kunne fjerne gamle feilmeldinger i et sånt tilfelle, er det lagt til støtte for å kunne spesifisere at en valideringsfeil er fikset. Da vil det aktuelle feltet kunne få beskjed om at en spesifikk feilmelding som den viser frem er fikset og skal skjules.
Dette gjøres ved å legge til en valideringsfeil i koden i det tilfellet der det ikke er noen feil i valideringen,
og sette *FIXED*
foran selve feilmeldingen. Dette tilsvarer oppsettet for myk validering.
Denne prefixen gjør at feilmeldingen som settes fjernes fra det aktuelle feltet, eller ignoreres (dersom det ikke er noen feilmelding på feltet fra før).
Man kan da utvide eksempelet over for å støtte dette:
private void ValidateFullName(Datamodell model, ModelStateDictionary validationResults)
{
if (!string.isNullOrEmpty(model.fornavn) && !string.isNullOrEmpty(model.etternavn)
&& model.fornavn.Length + model.etternavn.Length > 50)
{
validationResults.addModelError(nameof(model.fornavn),
"Fullt navn kan ikke være lengre enn 50 tegn.");
validationResults.addModelError(nameof(model.etternavn),
"Fullt navn kan ikke være lengre enn 50 tegn.");
}
else
{
validationResults.addModelError(nameof(model.fornavn),
"*FIXED*Fullt navn kan ikke være lengre enn 50 tegn.");
validationResults.addModelError(nameof(model.etternavn),
"*FIXED*Fullt navn kan ikke være lengre enn 50 tegn.");
}
}
Dersom du har problemer med å få dette til å fungere, og du ser valideringsmeldinger med *FIXED*
foran meldingen istedenfor at meldingen forsvinner,
bør du dobbeltsjekke at du har "FixedValidationPrefix": "*FIXED*"
satt under GeneralSettings
i appsettings.json
.
Myke valideringer
Myke valideringer er valideringsmeldinger som ikke stopper bruker fra å sende inn eller gå videre til neste steg i prosessen, men som benyttes til å gi brukeren ulike former for informasjon. Denne typen valideringer kan f.eks. brukes til å be brukeren om å verifisere input som virker feil eller rart, men som strengt tatt ikke er ugyldig, eller gi nyttig informasjon for videre utfylling.
Meldinger basert på myke validering vil vises en gang, men bruker kan velge å klikke seg videre uten å utføre endringer.
Myke valideringer legges til fra server-siden i validerings-logikken, på samme måte som vanlige validerings-feil. Forskjellen er at valideringsmeldingen
må prefixes med typen validering man ønker å gi, f.eks *WARNING*
. Dette vil da tolkes som en myk validering. Prefixen *WARNING*
blir ikke synlig for sluttbruker.
De tilgjengelige typene myke valideringer er WARNING
, INFO
og SUCCESS
.
Kodeeksempel
public async Task ValidateData(object data, ModelStateDictionary modelState)
{
if (data is TestModel testModel)
{
string firstName = testModel?.Person?.FirstName;
if (firstName != null && firstName.Contains("1337"))
{
validationResults.AddModelError(
"Person.FirstName",
"*WARNING*Are you sure your first name contains 1337?");
}
if (firstName != null && firstname.Contains("Altinn"))
{
validationResults.AddModelError(
"Person.FirstName",
"*SUCCESS*Altinn is a great name!");
}
}
await Task.CompletedTask;
}
Eksempler på visning av de ulike valieringene ser du nedenfor:
)
Det er også mulig å overstyre tittelen man ser på meldingene ved å legge til nøkklene soft_validation.info_title
, soft_validation.warning_title
, og soft_validation.success_title
i tekstressursene om man ønsker å sette custom tittel.
Gruppevalidering
Det er mulig å gjøre valideringer på en repeterende gruppe i det brukeren ønsker å lagre en gitt rad. Dersom det er valideringsfeil i raden, vil brukeren hindres fra å lukke raden til feilene er fikset.
{
"id": "demo-gruppe",
"type": "Group",
"children": [...],
"maxCount": 9,
"dataModelBindings": {...},
"validateOnSaveRow": ["All"],
}
Hvor validateOnSaveRow
inneholder et sett med validerings-typer som skal sjekkes; dette kan være én eller flere av:
Schema
Component
Expression
CustomBackend
Required
AllExceptRequired
All
{
"id": "demo-gruppe",
"type": "Group",
"children": [...],
"maxCount": 9,
"dataModelBindings": {...},
"triggers": ["validateRow"]
}
Om man legger til validering på gruppe-komponenten så vil det også gå et kall mot valideringen backend med en header som spesifiserer hvilken komponent som trigget valideringen: ComponentId
.
I tillegg er rad-indeksen for raden som blir lagret tilgjengelig i headeren RowIndex
. Dersom gruppen er en nøstet gruppe, er verdien en komma-separert liste med indekser, ellers er indeksen ett enkelt tall.
Valideringer er skrevet i C#, i ValidationHandler.cs
-filen i applikasjonsmalen. I valideringen kan man så hente ut komponent-id’en og skreddersy eventuelle valideringer som skal gjøres backend, eksempel:
public async Task ValidateData(object data, ModelStateDictionary validationResults)
{
if (data is flyttemelding model)
{
_httpContextAccessor.HttpContext
.Request.Headers
.TryGetValue("ComponentId", out StringValues compIdValues);
_httpContextAccessor.HttpContext
.Request.Headers
.TryGetValue("RowIndex", out StringValues rowIndexValues);
string componentId = compIdValues.FirstOrDefault(string.Empty);
switch (componentId)
{
case "top-level-group":
// kjør valideringer spesifikke til gruppen
// Hent rad-indeksen for en ikke-nøstet gruppe
int rowIndex = int
.Parse(rowIndexValues.FirstOrDefault(string.Empty));
break;
case "nested-group":
// Hent alle rad-indekser for en nøstet gruppe
int[] rowIndices = rowIndexValues
.FirstOrDefault(string.Empty)
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(s => int.Parse(s))
.ToArray();
break;
default:
// kjør valideringene i sin helhet
break;
}
}
}
For tips til hvordan man løser komplekse valideringer se ekemplene under enkeltfeltvalidering.