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