首页 > 解决方案 > NHibernate and weird casting exception

问题描述

I'm fighting it the second day and I'm just fed up.

I'm getting weird exceptions connected with my UI.

First things first. My model looks basically like that:

Base class:

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();
            }
        }
    }

}

Next we have PeriodBase class:

public enum PeriodType
{
    Year,
    Sheet
}

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

There are some more properties, but I just deleted them here for clarity.

Next, we have Period class that inherits from 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();
            }
        }
    }
}

Now, Period can have other periods and Sheets (which also inherites from 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();
            }
        }
    }
}

And finally we have document class, that is made up of Periods:

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();
            }
        }
    }
}

As you may guess, I get a tree hierarchy like that:

- Document
  - Period 1
    - Sheet 1

My bindings look like this:

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);
    }
}

For now, I just do eager loading everywhere. Just for simplicity.

Now WPF. This is how I create my TreeView (I'm using syncfusion controls):

<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>

And everything works until I save the records. It's just simple SaveAsync's in one transaction.

Everything gets saved but then I get a weird error. Application crashes with message: Cannot cast TreeViewItemAdv to PeriodBase.

What the heck? I can't even find the place when it's really throws. This is stacktrace from exception info:

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)

What's important, I get the same error after I start the application and load the document and click on the expander in treeview to expand Period. But everything works fine when I run the app for the first time, until I save the document.

What can be the problem?

In reply to Mark Feldman's post

I decided to reply in an answer as this is too long to comment. This is my first meeting with ORM, so I may have some wrong thoughts about this. I have just one model in my solution. Normally (using SQL) it would work. I would take an object, INSERT it into DB, and the other way also.

So I did the same way here. I just have one business model which has some simple business rules. It is used in ViewModels, and it's stored in db. Is it bad solution? Should I have another model and somewhat break DRY principle?

In my head it was suppose to work like this: User clicks "Create new Sheet". Here you are (this is part of my ViewModel -> method that is called from command):

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

This is more like pseudocode but it keeps the idea. Active document creates my sheet. This is done so because document signs to PropertyChanged event just to know if it was modified. Periods is ObservableCollection, so that I can react to adding and removing elements. Thanks to that period can set parentPeriod for my sheet automatically.

And then user saves it to db:

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

ModifiedItems is simply just a dictionary that keeps items that were modified. Thanks to this I don't have to save the whole document, just modified items.

So as far as I understand you this is not the way it should be. So what would be the PROPER way to do that? Or maybe ORM is not suitable here?

标签: wpfdata-bindingnhibernate

解决方案


除非在我使用 NHibernate 之后的几年里发生了重大变化,否则你不能仅仅从 ObservableModel 派生模型类并期望它能够工作。您这样做的理由似乎是为您的数据库模型提供 INPC,有些人会认为这不是很好的关注点分离,并表明您的视图模型层设计不正确。

也就是说,如果你真的坚持这样做,那么不要从 ObservableModel 派生你的实体,而是尝试在 NHibernate 首次创建实体时使用 Castle Dynamic Proxy 之类的东西将 INPC 注入你的实体。Ayende Rahien 的帖子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 会将您的集合反序列化为 type PersistentObservableGenericBag<T>

这就是您在运行时将 INPC 注入实体的方式,但是有几种方法可以完成您需要的操作,而无需实际执行。除了更容易实现之外,它们还不需要使用反射,如果您需要将代码迁移到不允许它的东西(例如 Xamarin.iOS),这是一个因素。添加基本​​ INPC 可以通过简单地添加ProprtyChanged.Fody来实现,它将在构建时自动将其添加到您的类属性 IL 中。对于更改集合,您最好将集合保留为 type IList<T>,在视图模型中用 type 类表示它们,ObserveableCollection<T>然后只需编写一些代码或辅助函数,以保持两者同步。

更新:我设法找到了获得该代码的原始项目,它是Fabio Maulo 的 uNhAddIns 项目的一部分。


推荐阅读