NHibernate и странное кастинг исключение - PullRequest
0 голосов
/ 18 апреля 2019

Я борюсь с этим второй день, и я просто сыт по горло.

Я получаю странные исключения, связанные с моим пользовательским интерфейсом.

Перво-наперво.Моя модель выглядит примерно так:

Базовый класс:

public class DbItem: ObservableModel
{
    public virtual Document ParentDocument { get; set; }

    Guid id;
    public virtual Guid Id
    {
        get { return id; }
        set
        {
            if (id != value)
            {
                id = value;
                NotifyPropertyChanged();
            }
        }
    }

    string name = string.Empty;
    public virtual string Name
    {
        get { return name; }
        set
        {
            if (value == null || name != value)
            {
                name = value;
                NotifyPropertyChanged();
            }
        }
    }

}

Далее у нас есть класс PeriodBase:

public enum PeriodType
{
    Year,
    Sheet
}

public abstract class PeriodBase : DbItem
{
    public virtual Period ParentPeriod { get; set; }
    public virtual PeriodType PeriodType { get; set; }
}

Есть еще несколько свойств, но я просто удалилих здесь для ясности.

Далее, у нас есть класс Period, который наследуется от PeriodBase:

public class Period : PeriodBase
{
    IList<PeriodBase> periods = new ObservableCollection<PeriodBase>();
    public virtual IList<PeriodBase> Periods
    {
        get { return periods; }
        set
        {
            if (periods != value)
            {
                periods = value;
                NotifyPropertyChanged();
            }
        }
    }
}

Теперь у Period могут быть другие периоды и листы (которые также наследуются от PeriodBase):

public class Sheet : PeriodBase
{
    DateTimeOffset startDate;
    public override DateTimeOffset StartDate
    {
        get { return startDate; }
        set
        {
            if (startDate != value)
            {
                startDate = value;
                NotifyPropertyChanged();
            }
        }
    }

    DateTimeOffset endDate;
    public override DateTimeOffset EndDate
    {
        get { return endDate; }
        set
        {
            if (endDate != value)
            {
                endDate = value;
                NotifyPropertyChanged();
            }
        }
    }
}

И, наконец, у нас есть класс документа, который состоит из периодов:

public class Document: DbItem
{
    IList<Period> periods = new ObservableCollection<Period>();
    public virtual IList<Period> Periods
    {
        get { return periods; }
        set
        {
            if (periods != value)
            {
                periods = value;
                NotifyPropertyChanged();
            }
        }
    }
}

Как вы можете догадаться, я получаю древовидную иерархию, подобную этой:

- Document
  - Period 1
    - Sheet 1

Мои привязки выглядят так:

public class DocumentMap : DbItemMap<Document>
{
    public DocumentMap()
    {
        Table("documents");
        HasMany(x => x.Periods).ForeignKeyConstraintName("ParentDocument_id");
    }
}


public class PeriodBaseMap: DbItemMap<PeriodBase>
{
    public PeriodBaseMap()
    {
        UseUnionSubclassForInheritanceMapping();
        References(x => x.ParentPeriod);
        Map(x => x.Name).Not.Nullable();
        Map(x => x.PeriodType).CustomType<PeriodType>();
    }
}

public class PeriodMap : SubclassMap<Period>
{
    public PeriodMap()
    {
        Table("periods");
        Abstract();
        References(x => x.ParentDocument);
        HasMany(x => x.Periods).Inverse().Not.LazyLoad();
    }
}

public class SheetMap : SubclassMap<Sheet>
{
    public SheetMap()
    {
        Table("sheets");
        Abstract();
        Map(x => x.StartDate);
        Map(x => x.EndDate);
    }
}

На данный момент я просто загружаюсь везде.Просто для простоты.

Теперь WPF.Вот как я создаю свой TreeView (я использую элементы управления syncfusion):

<sf:TreeViewAdv>
    <sf:TreeViewItemAdv  
            Header="Document" 
            LeftImageSource="../Resources/database.png" 
            ItemsSource="{Binding Periods}" 
            IsExpanded="True"
            >
        <sf:TreeViewItemAdv.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Periods}"> <!-- Period -->
                <TextBlock Text="{Binding Name}"/>
                <HierarchicalDataTemplate.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Name}"/> <!-- Sheet -->
                    </DataTemplate>
                </HierarchicalDataTemplate.ItemTemplate>
            </HierarchicalDataTemplate>
        </sf:TreeViewItemAdv.ItemTemplate>
    </sf:TreeViewItemAdv>
</sf:TreeViewAdv>

И все работает, пока я не сохраню записи.Это просто SaveAsync в одной транзакции.

Все сохраняется, но затем я получаю странную ошибку.Приложение аварийно завершает работу с сообщением: не удается привести TreeViewItemAdv к PeriodBase.

Какого черта?Я даже не могу найти место, когда оно действительно бросает.Это трассировка стека из информации об исключении:

in NHibernate.Collection.Generic.PersistentGenericBag`1.System.Collections.IList.IndexOf(Object value)
in System.Windows.Data.ListCollectionView.InternalIndexOf(Object item)
in Syncfusion.Windows.Tools.Controls.TreeViewItemAdv.Initialize(FrameworkTemplate template)
in Syncfusion.Windows.Tools.Controls.TreeViewItemAdv.TreeViewItemAdv_Loaded(Object sender, RoutedEventArgs e)
in System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised)
in System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args)
in System.Windows.BroadcastEventHelper.BroadcastEvent(DependencyObject root, RoutedEvent routedEvent)
in System.Windows.BroadcastEventHelper.BroadcastLoadedEvent(Object root)
in MS.Internal.LoadedOrUnloadedOperation.DoWork()
in System.Windows.Media.MediaContext.FireLoadedPendingCallbacks()
in System.Windows.Media.MediaContext.FireInvokeOnRenderCallbacks()
in System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget)
in System.Windows.Media.MediaContext.RenderMessageHandler(Object resizedCompositionTarget)
in System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
in System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)

Что важно, я получаю ту же ошибку после запуска приложения, загрузки документа и щелчка на расширителе в древовидном представлении, чтобы развернуть Period.Но все работает нормально, когда я запускаю приложение в первый раз, пока я не сохраню документ.

В чем может быть проблема?

В ответ на сообщение Марка Фельдмана

Я решил ответить в ответ, так как это слишком долго, чтобы комментировать.Это моя первая встреча с ORM, поэтому у меня могут быть некоторые неправильные мысли по этому поводу.У меня есть только одна модель в моем решении.Обычно (с использованием SQL) это будет работать.Я бы взял объект, вставил его в БД и другим способом.

Так что я сделал то же самое здесь.У меня просто есть одна бизнес-модель, в которой есть несколько простых бизнес-правил.Он используется во ViewModels и хранится в БД.Это плохое решение?Должен ли я иметь другую модель и несколько нарушить принцип СУХОГО?

В моей голове предполагалось работать так: Пользователь нажимает «Создать новый лист».Вот вы (это часть моего метода ViewModel ->, который вызывается из команды):

void CreateNewSheetInActiveDocument()
{
    Sheet sh = ActiveDocument.CreateItem<Sheet>();
    ActiveDocument.LastPeriod.Periods.Add(sh);
}

Это больше похоже на псевдокод, но оно сохраняет идею.Активный документ создает мой лист.Это сделано потому, что документ подписывается на событие PropertyChanged просто для того, чтобы узнать, было ли оно изменено.Periods - это ObservableCollection, так что я могу реагировать на добавление и удаление элементов.Благодаря этому периоду можно установить parentPeriod для моего листа автоматически.

И затем пользователь сохраняет его в db:

async Task SaveDocument(Document doc)
{
    foreach(var item in doc.ModifiedItems)
      db.SaveOrUpdate(item);
}

ModifiedItems - это просто словарь, в котором хранятся элементы, которые были изменены.Благодаря этому мне не нужно сохранять весь документ, только измененные элементы.

Так что, насколько я понимаю, это не так, как должно быть.Итак, что будет правильный способ сделать это?Или, может быть, ORM здесь не подходит?

Ответы [ 2 ]

1 голос
/ 19 апреля 2019

Если не произошло серьезных изменений в NHibernate за те годы, когда я его использовал, вы не можете просто извлечь классы моделей из ObservableModel и ожидать, что он будет работать. Похоже, что вы рассуждаете по этому поводу, чтобы дать INPC вашим моделям БД, что, как некоторые утверждают, не является хорошим разделением проблем, и предполагает, что ваш уровень модели представления не был спроектирован должным образом.

Тем не менее, если вы действительно непреклонны в этом, тогда вместо того, чтобы извлекать ваши сущности из ObservableModel, попробуйте использовать что-то вроде Castle Dynamic Proxy для внедрения INPC в ваши сущности, когда NHibernate впервые их создает. Пост Айенде Рахьен NHibernate & INotifyPropertyChanged показывает, как это сделать, а также предоставляет необходимый код.

Следующая проблема, с которой вы столкнетесь, - это проблема коллекций. Опять же, вы не можете просто присвоить ObservableCollection<T> свойству IList<T> и ожидать, что оно будет работать, NHibernate заменяет весь список, когда десериализует коллекции обратно, вместо использования добавления / удаления в уже существующей коллекции назначены. Можно заменить список на ObserveableCollection<T> после его загрузки, но если вы сделаете это, NHibernate будет думать, что весь список изменился, независимо от того, изменился он или нет, и снова сериализует все это обратно. Сначала вам это сойдет с рук, но довольно скоро удар по производительности начнет больно.

Чтобы обойти эту проблему, вам нужно будет использовать соглашение, чтобы NHibernate создавал объекты коллекций, которые поддерживают INotifyCollectionChanged. К сожалению, страница, где я изначально читал об этом, давно исчезла, поэтому мне придется просто опубликовать здесь код (к сожалению, без указания авторства). Я использовал соглашения только с NHibernate Fluent, поэтому я оставлю вас, чтобы узнать, как применять их в вашем случае, но вот что вам нужно ...

public class ObservableBagConvention : ICollectionConvention
{
    public void Apply(ICollectionInstance instance)
    {
        Type collectionType = typeof(ObservableBagType<>)
            .MakeGenericType(instance.ChildType);
        instance.CollectionType(collectionType);
        instance.LazyLoad();            
    }
}

public class ObservableBagType<T> : CollectionType, IUserCollectionType
{
    public ObservableBagType(string role, string foreignKeyPropertyName, bool isEmbeddedInXML)
        : base(role, foreignKeyPropertyName, isEmbeddedInXML)
    {
    }

    public ObservableBagType()
        : base(string.Empty, string.Empty, false)
    {

    }
    public IPersistentCollection Instantiate(ISessionImplementor session, ICollectionPersister persister)
    {
        return new PersistentObservableGenericBag<T>(session);
    }

    public override IPersistentCollection Instantiate(ISessionImplementor session, ICollectionPersister persister, object key)
    {
        return new PersistentObservableGenericBag<T>(session);
    }

    public override IPersistentCollection Wrap(ISessionImplementor session, object collection)
    {
        return new PersistentObservableGenericBag<T>(session, (ICollection<T>)collection);
    }

    public IEnumerable GetElements(object collection)
    {
        return ((IEnumerable)collection);
    }

    public bool Contains(object collection, object entity)
    {
        return ((ICollection<T>)collection).Contains((T)entity);
    }

    protected override void Clear(object collection)
    {
        ((IList)collection).Clear();
    }

    public object ReplaceElements(object original, object target, ICollectionPersister persister, object owner, IDictionary copyCache, ISessionImplementor session)
    {
        var result = (ICollection<T>)target;
        result.Clear();
        foreach (var item in ((IEnumerable)original))
        {
            if (copyCache.Contains(item))
                result.Add((T)copyCache[item]);
            else
                result.Add((T)item);
        }
        return result;
    }

    public override object Instantiate(int anticipatedSize)
    {
        return new ObservableCollection<T>();
    }

    public override Type ReturnedClass
    {
        get
        {
            return typeof(PersistentObservableGenericBag<T>);
        }
    }
}

Это код для соглашения, вы используете его с этим классом коллекции:

public class PersistentObservableGenericBag<T> : PersistentGenericBag<T>, INotifyCollectionChanged,
                                                 INotifyPropertyChanged, IList<T>
{
    private NotifyCollectionChangedEventHandler _collectionChanged;
    private PropertyChangedEventHandler _propertyChanged;

    public PersistentObservableGenericBag(ISessionImplementor sessionImplementor)
        : base(sessionImplementor)
    {
    }

    public PersistentObservableGenericBag(ISessionImplementor sessionImplementor, ICollection<T> coll)
        : base(sessionImplementor, coll)
    {
        CaptureEventHandlers(coll);
    }

    public PersistentObservableGenericBag()
    {
    }

    #region INotifyCollectionChanged Members

    public event NotifyCollectionChangedEventHandler CollectionChanged
    {
        add
        {
            Initialize(false);
            _collectionChanged += value;
        }
        remove { _collectionChanged -= value; }
    }

    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            Initialize(false);
            _propertyChanged += value;
        }
        remove { _propertyChanged += value; }
    }

    #endregion

    public override void BeforeInitialize(ICollectionPersister persister, int anticipatedSize)
    {
        base.BeforeInitialize(persister, anticipatedSize);
        CaptureEventHandlers(InternalBag);
    }

    private void CaptureEventHandlers(ICollection<T> coll)
    {
        var notificableCollection = coll as INotifyCollectionChanged;
        var propertyNotificableColl = coll as INotifyPropertyChanged;

        if (notificableCollection != null)
            notificableCollection.CollectionChanged += OnCollectionChanged;

        if (propertyNotificableColl != null)
            propertyNotificableColl.PropertyChanged += OnPropertyChanged;
    }

    private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        PropertyChangedEventHandler changed = _propertyChanged;
        if (changed != null) changed(this, e);
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler changed = _collectionChanged;
        if (changed != null) changed(this, e);
    }
}

И это все! Теперь NHibernate будет десериализовать ваши коллекции как PersistentObservableGenericBag<T>.

Так вот, как вы вводите INPC в сущности во время выполнения, но есть несколько способов выполнить то, что вам нужно, без необходимости делать это. Помимо того, что они проще в реализации, они также не требуют использования отражения, что является фактором, если вам когда-либо понадобится перенести ваш код в то, что не позволяет этого (например, Xamarin.iOS). Добавление базового INPC может быть достигнуто простым добавлением ProprtyChanged.Fody , который автоматически добавит его в свойства класса IL во время сборки. Для сбора изменений вам лучше оставить свои коллекции типа IList<T>, представив их классами типа ObserveableCollection<T> в моделях представления, а затем просто написав немного кода или вспомогательную функцию для синхронизации этих двух.

ОБНОВЛЕНИЕ: мне удалось отследить оригинальный проект, в котором я получил этот код, он является частью проекта uNhAddIns Фабио Мауло .

0 голосов
/ 23 апреля 2019

После изменений Марка Фельдмана ошибка все еще возникает.Но когда я изменил древовидный элемент управления на стандартный, проблема ушла.Это означает, что в контроле Syncfusion произошла ошибка.Я сообщил об этом.

...