Я пытаюсь воспользоваться новой функцией сложных свойств Entity Framework Core 8 для своего веб-приложения. Я превратил свою модель Address
, которая раньше была полноценной сущностью со своей собственной таблицей, в readonly record struct
, которая существует как комплексное свойство нескольких других моделей, например Customer
.
У меня есть компонент Blazor, который предназначен для включения в формы создания/редактирования и называется AddressSubForm
. Он содержит все поля формы, необходимые для адреса. Проблема в том, что раньше я мог просто @bind-Address = "Customer.Address"
, а теперь, когда Адрес равен readonly
, он не сможет изменить подсвойства Адреса, чтобы отразить ввод пользователя. Само по себе это не проблема: создать новый Адрес, используя синтаксис with
для создания новых записей путем изменения старых, несложно и несложно. Но у Blazor или Entity Framework (мне неясно, какой именно) сейчас возникают проблемы с отслеживанием изменений в адресе, и он сохраняет null
для всех столбцов адреса в базе данных.
Моя последняя попытка выглядит так:
@using System.Linq.Expressions
<FormItem Field = "Address">
<Template>
<label for = "Address" class = "k-label k-form-label">Address</label>
<div id = "Address" class = "k-form-field-wrap">
<p>
<label>Street Address</label>
<TelerikTextBox @bind-Value = "Street1"
OnChange = "async value => { Address = Address with { Street1 = (string)value }; await AddressChanged.InvokeAsync(); }" />
</p>
<p>
<label>Street Address (line 2)</label>
<TelerikTextBox @bind-Value = "Street2"
OnChange = "async value => { Address = Address with { Street2 = (string)value }; await AddressChanged.InvokeAsync(); }" />
</p>
<!-- ... -->
</div>
</Template>
</FormItem>
@code {
[Parameter] public Address Address { get; set; }
[Parameter] public EventCallback<Address> AddressChanged { get; set; }
[Parameter] public Expression<Func<Address>> AddressExpression { get; set; }
private string Street1 { get; set; } = string.Empty;
private string Street2 { get; set; } = string.Empty;
// ...
protected override void OnInitialized()
{
Street1 = Address.Street1;
Street2 = Address.Street2;
// ...
}
}
Как я могу изменить этот компонент, чтобы он правильно отслеживал любые изменения, внесенные в комплексное свойство «Адрес», и позволял сохранять его в базе данных?
Обратные вызовы OnChange
тоже неверны. Нет смысла так звонить AddressChanged
. Привязка данных Blazor сама генерирует эти события. Тот факт, что вам пришлось написать так много сложного кода, является очень убедительным признаком того, что вы что-то делаете неправильно.
(it's not clear to me which
Вы создаете совершенно новый Address
объект каждый раз, когда изменяется свойство, о котором ни EF, ни Blazor ничего не знают. Если используется автоматическая привязка данных, Blazor сам вызовет событие -Changed, когда Address = Address with{}
присваивает новый объект свойству Address
. Однако EF ничего об этом не знает. Вам придется снова прикрепить этот новый объект к DbContext.
@PanagiotisKanavos Я знаю, что делаю что-то неправильно, если бы я не делал что-то неправильно, у меня не было бы этой проблемы. Это была просто последняя попытка многих заставить это работать, и когда мой код начал выглядеть так, я понял, что мне нужно спросить. Раньше я слышал, что иногда явный вызов обратного вызова события может решить проблемы с двусторонней привязкой. Очевидно, в данном случае это не сработало.
Есть несколько подходов к этой довольно сложной теме.
Этот ответ основан на коде из моей недавней статьи. Он реализует концепцию контекста редактирования записи внутри подформы. Подформа использует дочернюю форму EditContext
.
Он также включает в себя правильное отслеживание состояния редактирования и блокировку форм.
Сначала объекты данных. Person — это стандартный объект типа DTO. Адрес — это объект значения записи. TrackState
является частью государственного отслеживания.
public class Person
{
[TrackState] public string? Name { get; set; }
// Tracking handled by the Sub-form
public Address? Address { get; set; }
}
public record Address(string Town, string PostCode);
Далее подформа. Он берет объект значения адреса, предоставленный привязкой, и создает AddressEditContext
для управления Address
редактированием объекта. Есть обширные встроенные комментарии, объясняющие, что происходит.
@using System.Linq.Expressions
<EditForm EditContext = "_editContext">
<label class = "form-label">Town</label>
<InputText class = "form-control mb-2"
@bind-Value=_recordEditContext.Town
@bind-Value:after = "OnPossibleChange" />
<label class = "form-label">PostCode</label>
<InputText class = "form-control mb-2"
@bind-Value=_recordEditContext.PostCode
@bind-Value:after = "OnPossibleChange" />
</EditForm>
@code {
[CascadingParameter] private EditContext ParentEditContext { get; set; } = default!;
[Parameter] public Address? Value { get; set; }
[Parameter] public EventCallback<Address> ValueChanged { get; set; }
[Parameter] public Expression<Func<Address>> ValueExpression { get; set; } = default!;
private Address _address = default!;
private EditContext _editContext = default!;
private AddressEditContext _recordEditContext = default!;
private Address? _previousState;
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
// Quick and dirty Exception checking
// Fix in production code
ArgumentNullException.ThrowIfNull(Value);
ArgumentNullException.ThrowIfNull(ParentEditContext);
ArgumentNullException.ThrowIfNull(ValueExpression);
// If _address is null, then this is the first render event
// build out the state objects from the provided address
if (_address is null)
{
_address = Value;
_recordEditContext = new(_address);
_previousState = _address;
_editContext = new(_recordEditContext);
}
return base.SetParametersAsync(ParameterView.Empty);
}
private async Task OnPossibleChange()
{
// Get the current state from the RecordEditContext
var state = _recordEditContext.AsRecord;
// Get the field identifier for the Address field
var fi = FieldIdentifier.Create(ValueExpression);
// If the state has changed:
// Update the state, notify the parent edit context and raise the ValueChanged callback
if (state != _previousState)
{
_previousState = state;
ParentEditContext.NotifyFieldChanged(fi);
await this.ValueChanged.InvokeAsync(state);
}
// Check if the address has reverted to the original state
// if so clear the Address from the EditContext modified list
if (!_recordEditContext.IsDirty)
ParentEditContext.MarkAsUnmodified(fi);
}
private class AddressEditContext
{
private Address _baseRecord;
public string Town { get; set; }
public string PostCode { get; set; }
public AddressEditContext(Address address)
{
_baseRecord = address;
Town = address.Town ?? string.Empty;
PostCode = address.PostCode ?? string.Empty;
}
public Address AsRecord
=> new(this.Town, this.PostCode);
public bool IsDirty => _baseRecord != this.AsRecord;
}
}
Наконец, демо-страница для редактирования Person. Я добавил функцию правильного отслеживания состояния с помощью BlazrEditStateTracker
и блокировки формы с помощью NavigationLock
.
@page "/"
@inject NavigationManager NavManager
<PageTitle>Home</PageTitle>
<h1>Composite Object Editor Demo</h1>
<EditForm EditContext = "_editContext">
<BlazrEditStateTracker/>
<label class = "form-label">Name</label>
<InputText class = "form-control mb-2"
@bind-Value=_model.Name />
<AddressEditor @bind-Value = "_model.Address" />
<div class = "text-end mb-2">
<button hidden = "@_isDirty" class = "btn btn-dark" @onclick = "this.Exit">Exit</button>
<button hidden = "@_isClean" class = "btn btn-danger" @onclick = "this.ExitWithoutSaving">Exit without Saving</button>
<button disabled = "@_isClean" class = "btn btn-success">Save</button>
</div>
</EditForm>
<NavigationLock ConfirmExternalNavigation = "_isDirty" OnBeforeInternalNavigation = "this.CheckNavigationAllowed" />
<div class = "bg-dark text-white m-2 p-2">
<pre>Name = @_model.Name</pre>
<pre>Town = @_model.Address?.Town</pre>
<pre>Postcode = @_model.Address?.PostCode</pre>
</div>
@code {
private Person _model = new() { Name = "Fred", Address = new("Hacconby", "PE10 OUX") };
private EditContext _editContext = default!;
private bool _isDirty => _editContext.IsModified();
private bool _isClean => !_isDirty;
private bool _exit;
protected override void OnInitialized()
{
_editContext = new(_model);
}
private void ExitWithoutSaving()
{
_exit = true;
this.NavManager.NavigateTo("/Counter");
}
private void Exit()
{
this.NavManager.NavigateTo("/Counter");
}
private void CheckNavigationAllowed(LocationChangingContext context)
{
if (_isDirty && !_exit)
context.PreventNavigation();
_exit = false;
}
}
Вот файл проекта, в котором показаны два пакета:
<Project Sdk = "Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include = "Blazr.EditStateTracker" Version = "1.3.0" />
</ItemGroup>
</Project>
Репозиторий Blazr.EditStateTracker находится здесь.
Мне удалось решить эту проблему, передав Func
этому компоненту, который устанавливает для свойства Address новое значение. Из-за этого мне больше не нужна двусторонняя привязка.
В AddressSubForm.razor
:
<FormItem Field = "Address">
<Template>
<label for = "Address" class = "k-label k-form-label">Address</label>
<div id = "Address" class = "k-form-field-wrap">
<p>
<label>Street Address</label>
<TelerikTextBox @bind-Value = "Street1"
OnChange = "value => Address = SetAddress(Address with { Street1 = (string)value })" />
</p>
<p>
<label>Street Address (line 2)</label>
<TelerikTextBox @bind-Value = "Street2"
OnChange = "value => Address = SetAddress(Address with { Street2 = (string)value })" />
</p>
<!-- ... -->
</div>
</Template>
</FormItem>
@code {
[Parameter] public Address Address { get; set; }
[Parameter] public Func<Address, Address> SetAddress { get; set; }
private string Street1 { get; set; } = string.Empty;
private string Street2 { get; set; } = string.Empty;
// ...
protected override void OnInitialized()
{
Street1 = Address.Street1;
Street2 = Address.Street2;
...
}
}
В AddCustomer.razor
:
<AddressSubForm Address = "Customer.Address"
SetAddress = "address => Customer.Address = address"/>
Здесь нет кода EF. Кроме того,
read-only
иtwo-way binding
предполагают недоразумение. Вам нужна двусторонняя привязка, если вы собираетесь обновить как свою ViewModel, так и объекты базы данных. Это означает, что они должны быть редактируемыми. Вы пытаетесь применить некоторые «лучшие практики»?