Source: DevExpress Blog

DevExpress Blog Connect a WinForms Data Grid to an Arbitrary ASP.NET Core WebAPI Service Powered by EF Core — Add Editing Features (Part 2)

I started a short series of posts a little while ago, titled Connect a WinForms Data Grid to an Arbitrary ASP.NET Core WebAPI Service Powered by EF Core. It relates to my previously published post Modern Desktop Apps And Their Complex Architectures and aims to illustrate an application system architecture that includes a data access service in addition to a WinForms application with a DevExpress Data Grid. The first post demonstrated the basic binding using the VirtualServerModeSource component, and this time I will explain how editing features can be activated in the Data Grid, interfacing with the same backend service. We need to add to the list of basic assumptions I described in the first post. My backend service uses EF Core for data access — I should point out that this is not too relevant! It makes it easy to demonstrate for a blog post how data modifications can be implemented in such a service, and it is of course a common choice. But the architecture and the binding to the frontend would be precisely the same if the service used some entirely different data storage system, or even if it worked on a platform other than .NET. Here’s the new assumption: the backend must allow for data to be modified through the accessible endpoints. There are various patterns for such data modifications, in this demo I chose to implement a REST-style access pattern that uses HTTP verbs to indicate the intent of a modification, and that accepts objects of the data transfer type through its endpoints (as opposed to some message or event based patterns, for instance).Table of ContentsIntro — Modern Desktop Apps And Their Complex Architecture | Choosing a Framework/App Architecture for Desktop & Mobile Cross-Platform Apps / GitHub samplePart 1 — Connect a WinForms Data Grid to an Arbitrary ASP.NET Core WebAPI Service Powered by EF Core — Architecture and Data Binding  / GitHub sample Part 2 — Connect a WinForms Data Grid to an Arbitrary ASP.NET Core WebAPI Service Powered by EF Core — Add Editing Features / GitHub sample (this post)Part 3 (TBD) — Connect a WinForms Data Grid to an Arbitrary ASP.NET Core WebAPI Service Powered by EF Core — Authenticate users and protect data / GitHub samplePart 4 — Connect a .NET Desktop Client to a Secure Backend Web API Service (EF Core with OData)Part 5 — Connect a .NET Desktop Client to a Backend Using a Middle Tier Server (EF Core without OData)Part 6 (TBD) — Connect a .NET Desktop Client to Azure Databases with Data API BuilderPart 7 (TBD) — Connect a .NET Desktop Client to GraphQL APIsWe also have related blog series, which may be of interest for you as well: JavaScript — Consume the DevExpress Backend Web API with Svelte (7 parts from data editing to validation, localization, reporting).​The demo repository You can find the sample code for this demo in the GitHub repository. The Readme file describes how to run the sample. Please contact us if you have any questions or comments! If you are interested in a few technical points about the sample code, please read on for a description of the sample structure and some of the relevant code files. New POST, PUT and DELETE endpoints Here are the three new endpoints: app.MapPost("/data/OrderItem", async (DataServiceDbContext dbContext, OrderItem orderItem) => { dbContext.OrderItems.Add(orderItem); await dbContext.SaveChangesAsync(); return Results.Created($"/data/OrderItem/{orderItem.Id}", orderItem); }); app.MapPut("/data/OrderItem/{id}", async (DataServiceDbContext dbContext, int id, OrderItem orderItem) => { if (id != orderItem.Id) { return Results.BadRequest("Id mismatch"); } dbContext.Entry(orderItem).State = EntityState.Modified; await dbContext.SaveChangesAsync(); return Results.NoContent(); }); app.MapDelete("/data/OrderItem/{id}", async (DataServiceDbContext dbContext, int id) => { var orderItem = await dbContext.OrderItems.FindAsync(id); if (orderItem is null) { return Results.NotFound(); } dbContext.OrderItems.Remove(orderItem); await dbContext.SaveChangesAsync(); return Results.NoContent(); }); These implementations are quite standard in the way they handle parameters and return values. The ASP.NET Core methods MapPost, MapPut, MapDelete, in addition to the MapGet that was already used in the previous version of the sample, establish the handling of different HTTP verbs with the requests. The runtime infrastructure of ASP.NET Core handles details automatically, such as the OrderItem parameter received in the MapPut handler, which is deserialized from its JSON representation using the existing EF Core type. The editing form The sample source code includes a simple editing form which will be used for both editing of existing rows and creating new rows. There are no surprises in this implementation, a couple of static methods provide an interface to call from the main form of the sample application. A new abstraction: the class DataServiceClient In my first post, I kept things simple and encoded the call to the data service, using an HttpClient instance, within the VirtualServerModeDataLoader class. A few lines of code handled the instantiation of the HttpClient with the correct base URL, and the process of fetching a specific URL and dealing with the JSON results. Since it now became clear that additional interface calls to the data service will be required, it made sense to abstract this logic and I created the new type DataServiceClient. The existing code in VirtualServerModeDataLoader now simply calls a method on the DataServiceClient: var dataFetchResult = await DataServiceClient.GetOrderItemsAsync( e.CurrentRowCount, BatchSize, SortField, SortAscending); Other methods exist in this class to handle the various use cases. Updates use the simplest algorithm, by encoding the transfer object as JSON and sending it to the service URL using a PUT request. public static async Task UpdateOrderItemAsync(OrderItem orderItem) { using var client = CreateClient(); var response = await client.PutAsync($"{baseUrl}/data/OrderItem/{orderItem.Id}", new StringContent(JsonSerializer.Serialize(orderItem), Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); } The process of creating a new item is a little bit more complicated. In this case, the service returns the object, which can be important if you allow values to be generated or modified on the server side during creation. In the demo setup, this applies to the primary key on the OrderItem EF type, which is an auto-generated int value. It is of course up to a client application to take advantage of this information where possible, but since the DataServiceClient is meant to be an example of a general purpose implementation it seems good practice to implement it fully. Note that this pattern introduces some overhead since it requires data to flow in both directions. It’s a good general recommendation to allow the client to generate key values by using a Guid type. But server-generated values are still common, so the implementation takes this into account. static OrderItem? AsOrderItem(this string responseBody) { return JsonSerializer.Deserialize<OrderItem>(responseBody, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } public static async Task<OrderItem?> CreateOrderItemAsync(OrderItem orderItem) { using var client = CreateClient(); var response = await client.PostAsync($"{baseUrl}/data/OrderItem", new StringContent(JsonSerializer.Serialize(orderItem), Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); return responseBody.AsOrderItem(); } Finally, for deletion there is more special handling in place. While both of the methods illustrated above detect any errors which occurred on the server (using the EnsureSuccessStatusCode helper), they don’t make any effort to handle such errors. In reality you may want to do a bit more work here, at least to log the errors — in the demo project I left this out for brevity. You could also choose to handle these cases like deletion, as I’m explaining now. On the UI level we have a chance to cancel an operation the user began (in this case deletion, but the idea also applies to creation or modification of data rows) if we find that it cannot complete as expected due to an error returned by the data service. You will see in a moment what the UI level code looks like, but in the DataServiceClient I implemented deletion a bit differently, by catching any errors and return a simple boolean status instead. public static async Task<bool> DeleteOrderItemAsync(int id) { try { using var client = CreateClient(); var response = await client.DeleteAsync($"{baseUrl}/data/OrderItem/{id}"); response.EnsureSuccessStatusCode(); return true; } catch (Exception ex) { Debug.WriteLine(ex); return false; } } The sensible action to log any errors for future analysis is indicated only by the Debug.WriteLine call in this code. The return value however allows the caller to find out whether the deletion was executed successfully, and react accordingly. The interface with the UI While the process of data loading, as I described in the previous post, used a specific built-in mechanism in the shape of the VirtualServerModeSource, there is no similar standard feature for editing operations. Instead, we nee

Read full article »
Est. Annual Revenue
$25-100M
Est. Employees
250-500
CEO Avatar

CEO

Ray Navasarkian

CEO Approval Rating

76/100

Read more