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 Contents
- Intro — Modern Desktop Apps And Their Complex Architecture | Choosing a Framework/App Architecture for Desktop & Mobile Cross-Platform Apps / GitHub sample
- Part 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 sample
- Part 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 Builder
- Part 7 (TBD) — Connect a .NET Desktop Client to GraphQL APIs
We 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
need to use the API of the Data Grid to handle click events for
editing, and to retrieve selected elements as needed.
To edit a row, I added a simple double-click event handler to
the GridView
.
private async void gridView1_DoubleClick(
object sender, EventArgs e)
{
if (sender is GridView view)
{
if (view.FocusedRowObject is OrderItem oi)
{
var editResult = EditForm.EditItem(oi);
if (editResult.changesSaved)
{
await DataServiceClient.UpdateOrderItemAsync(editResult.item);
view.RefreshData();
}
}
}
}
Using the property FocusedRowObject
on the view,
we retrieve the selected row by its object type. This object is
then passed to the API in the EditForm
, which
returns a flag indicating whether the user confirmed any edits
by clicking Save. In this case, the
DataServiceClient
comes in to persist the changed
object back to the data service. After a refresh of the view
data, the process is complete.
To enable adding of rows, I added a toolbar button to the form. Its click handler has this code, which is similar to the update logic.
private async void addItemButton_ItemClick(
object sender, DevExpress.XtraBars.ItemClickEventArgs e)
{
if (gridControl.FocusedView is ColumnView view)
{
var createResult = EditForm.CreateItem();
if (createResult.changesSaved)
{
await DataServiceClient.CreateOrderItemAsync(createResult.item!);
view.RefreshData();
}
}
}
A final toolbar button allows users to delete the focused
object. Again, we simply call into the
DataServiceClient
functionality to make this
happen.
private async void deleteItemButton_ItemClick(
object sender, DevExpress.XtraBars.ItemClickEventArgs e)
{
if (gridControl.FocusedView is ColumnView view &&
view.GetFocusedRow() is OrderItem orderItem)
{
await DataServiceClient.DeleteOrderItemAsync(orderItem.Id);
view.RefreshData();
}
}
That is all for this step. As you saw, both the implementation of the editing features in the service and the binding to the UI are easy to do. As ever, there may be some extra details to observe in a real-world solution, but the few lines of code this demo has so far brought us pretty far already!
Future plans for this post series
As I announced previously, the next post will cover authentication and authorization mechanisms that become necessary, but also possible, in the service-based application architecture I’m describing. In addition, I’m now planning a second upcoming post that describes how to activate various other Data Grid features, like Total Summaries and Filtering.
Please send your feedback — thanks to all of you who have already done that! — and any questions or ideas, we will attempt to consider everything!