Skip to content

Commit e716e18

Browse files
feat(grid): example for custom validation on edited items from the state (#87)
1 parent 78d764e commit e716e18

29 files changed

+1390
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.31205.134
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pre_validate_item_for_grid_state_init", "pre-validate-item-for-grid-state-init\pre_validate_item_for_grid_state_init.csproj", "{56752423-6C05-44E4-ACCA-F7717403520E}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{56752423-6C05-44E4-ACCA-F7717403520E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{56752423-6C05-44E4-ACCA-F7717403520E}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{56752423-6C05-44E4-ACCA-F7717403520E}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{56752423-6C05-44E4-ACCA-F7717403520E}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {C2E19C31-E4D7-4806-8009-6C0B811C274C}
24+
EndGlobalSection
25+
EndGlobal
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Router AppAssembly="typeof(Program).Assembly">
2+
<Found Context="routeData">
3+
<RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
4+
</Found>
5+
<NotFound>
6+
<h1>Page not found</h1>
7+
<p>Sorry, but there's nothing here!</p>
8+
</NotFound>
9+
</Router>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System;
2+
using System.ComponentModel.DataAnnotations;
3+
4+
namespace pre_validate_item_for_grid_state_init.Models
5+
{
6+
public class WeatherForecast
7+
{
8+
public int Id { get; set; }
9+
10+
public DateTime Date { get; set; }
11+
12+
private double _tempC { get; set; }
13+
public double TemperatureC
14+
{
15+
get
16+
{
17+
return _tempC;
18+
}
19+
set
20+
{
21+
_tempC = value;
22+
}
23+
}
24+
25+
public double TemperatureF
26+
{
27+
get
28+
{
29+
return 32 + (_tempC / 0.5556);
30+
}
31+
set
32+
{
33+
_tempC = (value - 32) * 0.5556;
34+
}
35+
}
36+
37+
[Required(ErrorMessage = "Your forecast requires a summary")]
38+
[StringLength(255, ErrorMessage = "Summaries can be no more than 255 characters")]
39+
public string Summary { get; set; }
40+
41+
public WeatherForecast()
42+
{
43+
Date = DateTime.Now.Date;
44+
}
45+
}
46+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
@page "/"
2+
3+
@using pre_validate_item_for_grid_state_init.Models
4+
@using pre_validate_item_for_grid_state_init.Services
5+
@using System.ComponentModel.DataAnnotations;
6+
@inject WeatherForecastService ForecastService
7+
8+
<TelerikButton OnClick="@CreateHandler">Add Forecast</TelerikButton>
9+
<TelerikButton OnClick="@DiscardHandler">Discard current forecast being edited with annotation message</TelerikButton>
10+
<TelerikTextArea @bind-Value="@TextAreaValue" Label="Message:" AutoSize="true" Enabled="false"></TelerikTextArea>
11+
<TelerikGrid @ref="grid" Data="@forecasts" Height="550px" FilterMode="@GridFilterMode.FilterMenu"
12+
Sortable="true" Pageable="true" PageSize="20" Groupable="true" Resizable="true" Reorderable="true"
13+
OnUpdate="@UpdateHandler" OnDelete="@DeleteHandler" EditMode="@GridEditMode.Incell">
14+
<GridColumns>
15+
<GridColumn Field="Id" Title="Id" Width="100px" Editable="false" Groupable="false" />
16+
<GridColumn Field="Date" Width="220px" DisplayFormat="{0:dddd, dd MMM yyyy}" />
17+
<GridColumn Field="TemperatureC" Title="Temp. C" DisplayFormat="{0:N1}" />
18+
<GridColumn Field="TemperatureF" Title="Temp. F" DisplayFormat="{0:N1}" />
19+
<GridColumn Field="Summary" />
20+
<GridCommandColumn Width="90px" Resizable="false">
21+
<GridCommandButton Command="Delete" Icon="delete"></GridCommandButton>
22+
</GridCommandColumn>
23+
</GridColumns>
24+
</TelerikGrid>
25+
26+
27+
@code {
28+
List<WeatherForecast> forecasts { get; set; }
29+
protected TelerikGrid<WeatherForecast> grid { get; set; }
30+
protected string TextAreaValue { get; set; }
31+
protected override async Task OnInitializedAsync()
32+
{
33+
await GetForecasts();
34+
}
35+
36+
async Task GetForecasts()
37+
{
38+
forecasts = await ForecastService.GetForecastListAsync(DateTime.Now);
39+
}
40+
41+
public async Task DeleteHandler(GridCommandEventArgs args)
42+
{
43+
TextAreaValue = "";
44+
WeatherForecast currItem = args.Item as WeatherForecast;
45+
46+
await ForecastService.DeleteForecastAsync(currItem);
47+
48+
await GetForecasts();
49+
}
50+
// Note that the "Add Forecast" button must not be in the grid (like the grid header)
51+
// If it is, it will be frozen and not clickable.
52+
public async Task CreateHandler()
53+
{
54+
TextAreaValue= ValidateGridState(true); // This will tell the user that there is a data problem with the grid that needs correcting
55+
if (TextAreaValue != "") return;
56+
57+
WeatherForecast currItem = new WeatherForecast();
58+
59+
await ForecastService.InsertForecastAsync(currItem);
60+
61+
await GetForecasts();
62+
}
63+
64+
public async Task DiscardHandler()
65+
{
66+
TextAreaValue = "";
67+
var gridState = grid.GetState();
68+
if (gridState.EditItem == null) return; // We are not editing anything so nothing to discard
69+
70+
ValidateGridState(false); // If you try to discard without calling this method, the grid will remain in a locked state
71+
72+
await ForecastService.DeleteForecastAsync(gridState.OriginalEditItem);
73+
74+
await GetForecasts();
75+
}
76+
77+
public async Task UpdateHandler(GridCommandEventArgs args)
78+
{
79+
TextAreaValue = "";
80+
WeatherForecast currItem = args.Item as WeatherForecast;
81+
82+
await ForecastService.UpdateForecastAsync(currItem);
83+
84+
await GetForecasts();
85+
}
86+
protected string ValidateGridState(bool isSave) // Change the bool name isSave for your needs
87+
{
88+
// When you click off a Telerik Grid in in-cell mode, there are cases where current edited cell does not close.
89+
// The two cases found thus far are when a cell fails validation via annotations or
90+
// when you are using an edit template like a DDL
91+
// this behavior with the editor template is documented in the editor template notes
92+
// https://docs.telerik.com/blazor-ui/components/grid/editing/incell#editor-template
93+
// For the cell validation case, Telerik locks the entire grid until you do something with the cell. Even if you
94+
// try to reload the grid on a discard, the grid will remain frozen even though the row/cell in question is gone.
95+
//
96+
// This function cleans up that mess by restoring the original item when you just want to discard
97+
// the current data or returns the data annotation error message on a save.
98+
// With this function, you can keep SAVE/DISCARD/CLOSE (or your CRUD functions) active for all grid edit types.
99+
// You could also use this approach before loading state into the grid (in OnStateInit) to ensure your users will have
100+
// a workable grid even if they left off in an invalid state when they saved their session
101+
// In this example the method returns a string that is put in a textarea, you can show it to the user
102+
// in any other way, such as with a TelerikNotification or a Telerik alert dialog
103+
104+
if (grid == null) return "";
105+
106+
var gridState = grid.GetState();
107+
if (gridState.EditItem != null)
108+
{
109+
if (isSave)
110+
{
111+
// For the save case, validate the EditItem object
112+
113+
// This post was very helpful in figuring this out: https://www.c-sharpcorner.com/UploadFile/20c06b/using-data-annotations-to-validate-models-in-net/
114+
ICollection<ValidationResult> results = new List<ValidationResult>();
115+
if (!Validator.TryValidateObject(gridState.EditItem, new ValidationContext(gridState.EditItem), results, true))
116+
{
117+
// Failed validation - should just be one failure since we are doing in-cell editing
118+
return String.Join("", results.Select(o => $"- {o.ErrorMessage}"));
119+
}
120+
}
121+
else
122+
{
123+
// Discard case - just reset the field with the original value to allow the
124+
// grid to be in a good state if the field in question failed validation via annotations
125+
var currentDataFields = gridState.EditItem; // Get the row data containing the cell that is being edited
126+
var originalDataFields = gridState.OriginalEditItem; // Get the original data
127+
var currentDataField = currentDataFields.GetType().GetProperty(gridState.EditField); // EditField is the name of the field we are editing (string name)
128+
var originalDataField = originalDataFields.GetType().GetProperty(gridState.EditField);
129+
var originalDataValue = originalDataField.GetValue(originalDataFields);
130+
currentDataField.SetValue(currentDataFields, originalDataValue); // Reset the current value with the original value
131+
}
132+
}
133+
return "";
134+
}
135+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
@page "/"
2+
@namespace pre_validate_item_for_grid_state_init.Pages
3+
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
4+
5+
<!DOCTYPE html>
6+
<html lang="en">
7+
<head>
8+
<meta charset="utf-8" />
9+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
10+
<title>GridAndMenuServerApp</title>
11+
<base href="~/" />
12+
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
13+
<link href="css/site.css" rel="stylesheet" />
14+
15+
<link rel="stylesheet" href="_content/Telerik.UI.for.Blazor/css/kendo-theme-bootstrap/all.css" />
16+
<script src="_content/Telerik.UI.for.Blazor/js/telerik-blazor.js" defer></script>
17+
</head>
18+
<body>
19+
<component type="typeof(App)" render-mode="ServerPrerendered" />
20+
21+
<div id="blazor-error-ui">
22+
<environment include="Staging,Production">
23+
An error has occurred. This application may no longer respond until reloaded.
24+
</environment>
25+
<environment include="Development">
26+
An unhandled exception has occurred. See browser dev tools for details.
27+
</environment>
28+
<a href class="reload">Reload</a>
29+
<a class="dismiss">🗙</a>
30+
</div>
31+
32+
<script src="_framework/blazor.server.js"></script>
33+
</body>
34+
</html>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using Microsoft.AspNetCore;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Threading.Tasks;
11+
12+
namespace pre_validate_item_for_grid_state_init
13+
{
14+
public class Program
15+
{
16+
public static void Main(string[] args)
17+
{
18+
CreateHostBuilder(args).Build().Run();
19+
}
20+
21+
public static IHostBuilder CreateHostBuilder(string[] args) =>
22+
Host.CreateDefaultBuilder(args)
23+
.ConfigureWebHostDefaults(webBuilder =>
24+
{
25+
webBuilder.UseStaticWebAssets();
26+
webBuilder.UseStartup<Startup>();
27+
});
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:64139/",
7+
"sslPort": 44351
8+
}
9+
},
10+
"profiles": {
11+
"IIS Express": {
12+
"commandName": "IISExpress",
13+
"launchBrowser": true,
14+
"environmentVariables": {
15+
"ASPNETCORE_ENVIRONMENT": "Development"
16+
}
17+
},
18+
"pre_validate_item_for_grid_state_init": {
19+
"commandName": "Project",
20+
"launchBrowser": true,
21+
"environmentVariables": {
22+
"ASPNETCORE_ENVIRONMENT": "Development"
23+
},
24+
"applicationUrl": "https://localhost:5001;http://localhost:5000"
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using pre_validate_item_for_grid_state_init.Models;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
7+
namespace pre_validate_item_for_grid_state_init.Services
8+
{
9+
public class WeatherForecastService
10+
{
11+
private static readonly string[] Summaries = new[]
12+
{
13+
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
14+
};
15+
16+
private List<WeatherForecast> _forecasts { get; set; }
17+
18+
public Task<List<WeatherForecast>> GetForecastListAsync(DateTime startDate)
19+
{
20+
if (_forecasts == null)
21+
{
22+
var rng = new Random();
23+
_forecasts = Enumerable.Range(1, 150).Select(index => new WeatherForecast
24+
{
25+
Id = index,
26+
Date = startDate.AddDays(index),
27+
TemperatureC = rng.Next(-20, 55),
28+
Summary = Summaries[rng.Next(Summaries.Length)]
29+
}).ToList();
30+
}
31+
32+
// detach the View data from the service for this example by creating a new pointer
33+
List<WeatherForecast> cloneForTemplate = new List<WeatherForecast>(_forecasts);
34+
return Task.FromResult<List<WeatherForecast>>(cloneForTemplate);
35+
}
36+
37+
public async Task UpdateForecastAsync(WeatherForecast forecastToUpdate)
38+
{
39+
//implement proper error handling here, and then actual data source operations
40+
if (_forecasts == null)
41+
{
42+
return;
43+
}
44+
45+
var index = _forecasts.FindIndex(i => i.Id == forecastToUpdate.Id);
46+
if (index != -1)
47+
{
48+
_forecasts[index] = forecastToUpdate;
49+
}
50+
}
51+
52+
public async Task DeleteForecastAsync(WeatherForecast forecastToRemove)
53+
{
54+
if (_forecasts == null) return;
55+
//implement proper error handling here, and then actual data source operations
56+
57+
_forecasts.Remove(forecastToRemove);
58+
}
59+
60+
public async Task InsertForecastAsync(WeatherForecast forecastToInsert)
61+
{
62+
//implement proper error handling here, and then actual data source operations
63+
if (_forecasts == null)
64+
{
65+
return;
66+
}
67+
68+
WeatherForecast insertedForecast = new WeatherForecast()
69+
{
70+
Id = _forecasts.Count + 1,
71+
Date = forecastToInsert.Date,
72+
TemperatureC = forecastToInsert.TemperatureC,
73+
Summary = forecastToInsert.Summary
74+
};
75+
76+
_forecasts.Insert(0, insertedForecast);
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)