Я пытаюсь воспользоваться новой функцией сложных свойств 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, так и объекты базы данных. Это означает, что они должны быть редактируемыми. Вы пытаетесь применить некоторые «лучшие практики»?