#c# #sql-server #wpf #entity-framework #data-binding
#c# #sql-сервер #wpf #entity-framework #привязка данных
Вопрос:
Я неделями боролся с моим приложением WPF с именем «ContactManager», когда я хочу добавить записи в базу данных. У меня есть два объекта:
public partial class Contact : EntityBase
{
public int ContactId { get; set; }
[StringLength(20)]
public string FirstName { get; set; }
[StringLength(20)]
public string LastName { get; set; }
[StringLength(20)]
public string Organization { get; set; }
[StringLength(20)]
public string JobTitle { get; set; }
public string ImagePath { get; set; }
public string CellPhone { get; set; }
public string HomePhone { get; set; }
public string OfficePhone { get; set; }
public string PrimaryEmail { get; set; }
public string SecondaryEmail { get; set; }
public virtual Address Address { get; set; } = new Address();
public class Address : EntityBase
{
[ForeignKey("Contact")]
public int AddressId { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public string Zip { get; set; }
public string State { get; set; }
public virtual Contact Contact { get; set; }
Метод добавления:
public int Add(Contact entity)
{
Context.Contacts.Add(entity);
var o = SaveChanges();
Context.Dispose();
Context = new CMEntities();
return o;
}
internal int SaveChanges()
{
try
{
return Context.SaveChanges();
}
}
Когда база данных пуста, она работает, но после закрытия и перезапуска приложения я получаю следующее исключение:
Исключение SQLException: нарушение ограничения ПЕРВИЧНОГО КЛЮЧА ‘PK_dbo.Адреса’. Не удается вставить дубликат ключа в объект ‘dbo.Адреса’. Значение дубликата ключа равно (1). Оператор был завершен.
Я не понимаю, почему Sql server (или entity framework ??) хочет вставить ключ 1 вместо следующего идентификатора…
если я удаляю строки в таблицах адресов, он снова работает, он без проблем вставляет строку со следующим / правильным адресом.(то есть addressid равен не 1, а 5 из-за предыдущих строк контактов)
На мой взгляд, схема таблиц (ключей и т. Д.) Верна, Проблема связана с другой проблемой, возможно, привязкой или контекстом, я понятия не имею…
(Я сделал более простой пример, опустив MVC, он работает. )
Вот код приложения MVC:
namespace CMnew.Model
{
public partial class ContactRepository : IDisposable
{
public CMEntities Context { get; set; } = new CMEntities();
private List<Contact> _contactStore;
public ContactRepository()
{
if (this.GetAll() == null)
{
_contactStore = new List<Contact>();
}
else
{
_contactStore = this.GetAll();
}
}
public List<Contact> FindByLookup(string lookupName)
{
IEnumerable<Contact> found = from c in _contactStore
where c.LookupName.StartsWith(lookupName, StringComparison.OrdinalIgnoreCase)
select c;
return found.ToList();
}
public List<Contact> FindAll()
{
return new List<Contact>(_contactStore);
}
public void Save(Contact contact)
{
if (_contactStore.Contains(contact))
{
this.SaveToDatabase(contact);
}
else
{
_contactStore.Add(contact);
this.Add(contact);
}
}
public int SaveToDatabase(Contact entity)
{
return SaveChanges();
}
public void Delete(Contact contact)
{
_contactStore.Remove(contact);
DeleteFromDatabase(contact);
}
public int DeleteFromDatabase(Contact entity)
{
Context.Entry(entity).State = EntityState.Deleted;
return SaveChanges();
}
public Contact GetOne(int? id) => Context.Contacts.Find(id);
public List<Contact> GetAll() => Context.Contacts.ToList();
public int Add(Contact entity)
{
Context.Contacts.Add(entity);
var o = SaveChanges();
Context.Dispose();
Context = new CMEntities();
return o;
}
internal int SaveChanges()
{
try
{
return Context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
throw;
}
catch (DbUpdateException ex)
{
throw;
}
catch (CommitFailedException ex)
{
throw;
}
catch (Exception ex)
{
throw ex;
}
}
bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
Context.Dispose();
}
disposed = true;
}
}
}
namespace CMnew.Presenters
{
public class ApplicationPresenter : PresenterBase<Shell>, INotifyPropertyChanged
{
private readonly ContactRepository _contactRepository;
private ObservableCollection<Contact> _currentContacts;
public event PropertyChangedEventHandler PropertyChanged;
public ApplicationPresenter(Shell view, ContactRepository contactRepository) : base(view)
{
_contactRepository = contactRepository;
_currentContacts = new ObservableCollection<Contact>(_contactRepository.FindAll());
}
// public ObservableCollection<Contact> CurrentContacts { get; set; }
public ObservableCollection<Contact> CurrentContacts
{
get { return _currentContacts; }
set
{
_currentContacts = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentContacts)));
}
}
public string StatusText { get; set; }
public void Search(string criteria)
{
if (!string.IsNullOrEmpty(criteria) amp;amp; criteria.Length > 2)
{
CurrentContacts = new ObservableCollection<Contact>(_contactRepository.FindByLookup(criteria));
StatusText = string.Format("{0} contacts found.", CurrentContacts.Count);
}
else
{
CurrentContacts = new ObservableCollection<Contact>(_contactRepository.FindAll());
}
}
public void NewContact()
{
OpenContact(new Contact());
}
public void SaveContact(Contact contact)
{
if (!CurrentContacts.Contains(contact))
{
CurrentContacts.Add(contact);
}
_contactRepository.Save(contact);
StatusText = string.Format("Contact '{0}' was saved.", contact.LookupName);
}
public void DeleteContact(Contact contact)
{
if (CurrentContacts.Contains(contact))
{
CurrentContacts.Remove(contact);
}
_contactRepository.Delete(contact);
StatusText = string.Format("Contact '{0}' was deleted.", contact.LookupName);
}
public void CloseTab<T>(PresenterBase<T> presenter)
{
View.RemoveTab(presenter);
}
private void OpenContact(Contact contact)
{
if (contact == null) return;
View.AddTab(new EditContactPresenter(this, new EditContactView(), contact));
}
public void DisplayAllContacts()
{
throw new NotImplementedException();
}
}
}
namespace CMnew.Presenters
{
public class EditContactPresenter : PresenterBase<EditContactView>
{
private readonly ApplicationPresenter _applicationPresenter;
private Contact _contact;
public EditContactPresenter(ApplicationPresenter applicationPresenter, EditContactView view, Contact contact) : base(view, "Contact.LookupName")
{
_applicationPresenter = applicationPresenter;
_contact = contact;
}
public Contact Contact
{
get { return _contact; }
set { _contact = value; }
}
public void SelectImage()
{
string imagePath = View.AskUserForImagePath();
if (!string.IsNullOrEmpty(imagePath))
{
Contact.ImagePath = imagePath;
}
}
public void Save()
{
_applicationPresenter.SaveContact(Contact);
}
public void Delete()
{
_applicationPresenter.CloseTab(this);
_applicationPresenter.DeleteContact(Contact);
}
public void Close()
{
_applicationPresenter.CloseTab(this);
}
public override bool Equals(object obj)
{
EditContactPresenter presenter = obj as EditContactPresenter;
return presenter != null amp;amp; presenter.Contact.Equals(Contact);
}
}
}
namespace CMnew.Views
{
/// <summary>
/// Interaction logic for EditContactView.xaml
/// </summary>
public partial class EditContactView : UserControl
{
public EditContactView()
{
InitializeComponent();
}
public EditContactPresenter Presenter
{
get { return DataContext as EditContactPresenter; }
}
private void Save_Click(object sender, RoutedEventArgs e)
{
Presenter.Save();
}
private void Delete_Click(object sender, RoutedEventArgs e)
{
Presenter.Delete();
}
private void Close_Click(object sender, RoutedEventArgs e)
{
Presenter.Close();
}
private void SelectImage_Click(object sender, RoutedEventArgs e)
{
Presenter.SelectImage();
}
public string AskUserForImagePath()
{
OpenFileDialog dlg = new OpenFileDialog();
dlg.ShowDialog();
return dlg.FileName;
}
}
}
<UserControl x:Class="CMnew.Views.EditContactView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CMnew.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<DockPanel Margin="5">
<Border DockPanel.Dock="Top">
<DockPanel LastChildFill="False">
<TextBlock DockPanel.Dock="Left" Text="{Binding Contact.LastName}"/>
<TextBlock DockPanel.Dock="Left" Text=", "/>
<TextBlock DockPanel.Dock="Left" Text="{Binding Contact.FirstName}"/>
<TextBlock DockPanel.Dock="Right" Text="{Binding Contact.Organization}"/>
</DockPanel>
</Border>
<StackPanel DockPanel.Dock="Bottom" Style="{StaticResource buttonPanel}">
<Button Content="Save" Click="Save_Click"/>
<Button Content="Delete" Click="Delete_Click"/>
<Button Content="Close" Click="Close_Click"/>
</StackPanel>
<WrapPanel>
<GroupBox BorderBrush="{StaticResource lightBlueBrush}">
<GroupBox.Header>
<Border Background="{StaticResource lightBlueBrush}" Style="{StaticResource groupBoxHeader}">
<TextBlock Text="General"/>
</Border>
</GroupBox.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="175"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.RowSpan="4">
<Border Background="Gray"
CornerRadius="6"
Margin="2 2 0 0"
Opacity=".5"/>
<Border Margin="2 2 4 4"
Background="White"/>
<Viewbox Margin="2 2 4 4">
<Image Source="{Binding Contact.ImagePath}" />
</Viewbox>
<Border BorderBrush="{StaticResource lightBlueBrush}"
BorderThickness="2"
Background="Transparent"
CornerRadius="6"
Margin="0 0 2 2"/>
<Button Style="{StaticResource openButton}"
Background="White"
Foreground="{StaticResource lightBlueBrush}"
BorderBrush="{StaticResource lightBlueBrush}"
ToolTip="Change Picture"
Click="SelectImage_Click" />
</Grid>
<Label Grid.Column="1"
Content="_First Name:"
Target="{Binding ElementName=firstName}"/>
<TextBox x:Name="firstName"
Grid.Column="2"
Text="{Binding Contact.FirstName}"/>
<Label Grid.Row="1"
Grid.Column="1"
Content="_Last Name:"
Target="{Binding ElementName=lastName}"/>
<TextBox x:Name="lastName"
Grid.Row="1"
Grid.Column="2"
Text="{Binding Contact.LastName}"/>
<Label Grid.Row="2"
Grid.Column="1"
Content="Or_ganization:"
Target="{Binding ElementName=organization}"/>
<TextBox x:Name="organization"
Grid.Row="2"
Grid.Column="2"
Text="{Binding Contact.Organization}"/>
<Label Grid.Row="3"
Grid.Column="1"
Content="_Job Title:"
Target="{Binding ElementName=jobTitle}"/>
<TextBox x:Name="jobTitle"
Grid.Row="3"
Grid.Column="2"
Text="{Binding Contact.JobTitle}"/>
</Grid>
</GroupBox>
<GroupBox BorderBrush="{StaticResource greenBrush}">
<GroupBox.Header>
<Border Background="{StaticResource greenBrush}"
Style="{StaticResource groupBoxHeader}">
<TextBlock Text="Address"/>
</Border>
</GroupBox.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="150"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Content="Line _1:"
Target="{Binding ElementName=line1}" />
<TextBox x:Name="line1"
Grid.Column="1"
Grid.ColumnSpan="3"
Text="{Binding Contact.Address.Line1}" />
<Label Grid.Row="1"
Content="Line _2:"
Target="{Binding ElementName=line2}" />
<TextBox x:Name="line2"
Grid.Row="1"
Grid.Column="1"
Grid.ColumnSpan="3"
Text="{Binding Contact.Address.Line2}" />
<Label Grid.Row="2"
Content="Ci_ty:"
Target="{Binding ElementName=city}" />
<TextBox x:Name="city"
Grid.Row="2"
Grid.Column="1"
Text="{Binding Contact.Address.City}" />
<Label Grid.Row="2"
Grid.Column="2"
Content="_State:"
Target="{Binding ElementName=state}" />
<TextBox x:Name="state"
Grid.Row="2"
Grid.Column="3"
Text="{Binding Contact.Address.State}" />
<Label Grid.Row="3"
Grid.Column="0"
Content="_Zip:"
Target="{Binding ElementName=zip}" />
<TextBox x:Name="zip"
Grid.Row="3"
Grid.Column="1"
Text="{Binding Contact.Address.Zip}" />
<Label Grid.Row="3"
Grid.Column="2"
Content="Countr_y:"
Target="{Binding ElementName=country}" />
<TextBox x:Name="country"
Grid.Row="3"
Grid.Column="3"
Text="{Binding Contact.Address.Country}" />
</Grid>
</GroupBox>
<GroupBox BorderBrush="{StaticResource redBrush}">
<GroupBox.Header>
<Border Background="{StaticResource redBrush}"
Style="{StaticResource groupBoxHeader}">
<TextBlock Text="Phone"/>
</Border>
</GroupBox.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="150"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Content="_Office:"
Target="{Binding ElementName=office}"/>
<TextBox x:Name="office"
Grid.Column="1"
Text="{Binding Contact.OfficePhone}" />
<Label Grid.Row="1"
Content="_Cell:"
Target="{Binding ElementName=cell}" />
<TextBox x:Name="cell"
Grid.Row="1"
Grid.Column="1"
Text="{Binding Contact.CellPhone}" />
<Label Grid.Row="2"
Content="_Home:"
Target="{Binding ElementName=home}" />
<TextBox x:Name="home"
Grid.Row="2"
Grid.Column="1"
Text="{Binding Contact.HomePhone}" />
</Grid>
</GroupBox>
<GroupBox BorderBrush="{StaticResource brownBrush}">
<GroupBox.Header>
<Border Background="{StaticResource brownBrush}"
Style="{StaticResource groupBoxHeader}">
<TextBlock Text="Email"/>
</Border>
</GroupBox.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Content="_Primary:"
Target="{Binding ElementName=primaryEmail}"/>
<TextBox x:Name="primaryEmail"
Grid.Column="1"
Text="{Binding Contact.PrimaryEmail}"/>
<Label Grid.Row="1"
Content="S_econdary:"
Target="{Binding ElementName=secondaryEmail}"/>
<TextBox x:Name="secondaryEmail"
Grid.Row="1"
Grid.Column="1"
Text="{Binding Contact.SecondaryEmail}"/>
</Grid>
</GroupBox>
</WrapPanel>
</DockPanel>
</UserControl>
Комментарии:
1. Ошибка, скорее всего, в коде, который вы не показываете. Возможно, у вас есть несколько контактов, указывающих на одни и те же объекты addres, которые вы затем добавляете дважды.
2. я добавил больше кода, пожалуйста, сосредоточьтесь только на методе добавления / сохранения, обновление и удаление еще не закодированы правильно. Спасибо.
3. Какая версия EF?
4. Это сначала код? Я бы всегда рекомендовал, по крайней мере, сначала начать с базы данных. Как ваш linq в конечном итоге генерирует вставку для адреса с идентификатором адреса в нем вообще? Во вставке не должно быть идентификатора вообще, чтобы таблица имела столбец идентификаторов. Я бы также явно применил атрибуты [Key], а не полагался на соглашение. Легче читать, если ничего другого. Скопируйте данные объекта в viewmodels и поместите в них проверки и примечания к данным, а не в классы ef.
5. В методе «Add» поставьте точку останова в строке var o = SaveChanges(); и посмотрите, как выглядит входящий объект «entity». Его тип Contact . Попробуйте проверить поле «AddressID» в окне отладчика. какие значения поступают?
Ответ №1:
Вероятно, это ваша проблема:
public class Address : EntityBase
{
[ForeignKey("Contact")] // <---
public int AddressId { get; set; }
наряду с этим вы удаляете свой DbContext после сохранения. Когда EF DbContext обнаруживает новую сущность (Контакт) со связанной сущностью, которая, как вы ожидаете, существует, но не отслеживается, она будет рассматривать эту сущность как новую сущность. Такое поведение, вероятно, является причиной того, что мешает вам передавать объекты. Адрес больше не отслеживается, поэтому предпринимается попытка повторно вставить существующую запись адреса, и поскольку это FK, указывающий на контакт, он не будет генерировать новое значение, а попытается использовать текущий идентификатор контакта.
Обычно адрес должен иметь PK (AddressID) и FK (ContactID), если у вашего контакта может быть несколько адресов. Либо это, либо объект Contact будет иметь AddressID. (Где один адрес может обслуживать несколько контактов) Если вы хотите, чтобы у одного контакта был только один адрес, то либо поместите поля адреса в сам контакт, либо создайте таблицу ContactAddressDetails с ContactID в качестве PK FK для HasRequired
WithRequired
отношения /, если вы хотите, чтобы это было в отдельной таблице. Желаемая связь будет определять, как будут выглядеть поля в сущности / схеме.
Комментарии:
1. Спасибо за ответ, но я не думаю, что это проблема, потому что почему наше приложение работает при первом запуске?? При втором запуске приложения я ничего не удаляю, потому что получаю сообщение об ошибке перед удалением инструкции…
2. Это ядро EF6 или EF? для EF6 необходимо настроить взаимно однозначные отношения, соединяющие первичные ключи, которые выглядят так, как вы их подключаете, но это может привести к путанице с разными именами ключей (идентификатор контакта = идентификатор адреса), поэтому либо приведите поля адресов в контакт, либо вызовите таблицу что-то вроде ContactAddressDetails сPK идентификатора контакта. В любом случае, попробуйте явно сопоставить требуемый. Требуется сопоставление между таблицами либо при инициализации DbContext, либо при настройке EntityTypeConfiguration…
3. Из вашего примера также неясно, где / как объект адреса выделяется для нового контакта. В случае 1 к одному адрес должен быть создан везде, где также создается Контакт. (Т.е. фабричный метод
CreateContract
должен делать что-то вродеvar contract = new Contract { Address = new Address { /* ... */ }, /* ... */ }; return contract;
Ответ №2:
Спасибо за ваши ответы, но я нашел проблему:
public ContactRepository()
{
if (this.GetAll() == null)
{
_contactStore = new List<Contact>();
}
else
{
_contactStore = this.GetAll();
}
}
public List<Contact> GetAll() => Context.Contacts.ToList();
В конструкторе ContactRepository после вызова GetAll () я должен удалить текущий контекст:
private List<Contact> _contactStore = new List<Contact>();
public ContactRepository()
{
_contactStore = this.GetAll();
Context.Dispose();
Context = new CMEntities();
}
и все работает нормально.
я не знаю, есть ли лучшее решение вместо непрерывного вызова Context.Dispose () или это правильный путь?