public class Base : ICustomItem
{
}
public class Book : Base
{
public List<Author> Authors;
public List<Bookmark> Bookmarks;
}
public class Author : Base
{
}
public class Bookmark : Base
{
}
public ObservableCollection(ICustomItem) MyItems;
public ListCollectionView MyItemsView { get; } = new ListCollectionView(MyItems);
使用这样的设置,我可以显示包含书籍,作者和书签的单数列表。我可以深入研究一本书,并查看特定的书作者和书签。
问题1:我想深入研究一位作者,并查看所有作者的书。最重要的是,我想查看该作者所有书籍的所有书签。
一个简单的解决方案是将适当的列表添加到彼此的类中。例如。
public List<Book> Books
到Author
类。但是,当您开始添加更多类别(例如Genre
,Publisher
,Language
等)时,这变得不受欢迎
问题2:我还希望能够按任何选定标签的编号对列表进行排序,包括任何相关标签类型:
MyItemsView.SortDescriptions.Add(new SortDescription("Bookmarks.Count", Descending);
作者拥有的书签数量应该是其所有书籍的所有书签的总和。
什么是构造此类数据,而不必为每种类型维护多个查找集合的好方法?使用Book
作为真理的来源不是一个好方法吗?理想情况下,我可以按任何标记类型进行排序。
在我的实际应用程序中,我已经解决了问题1:深入研究例如。一个Author
,我在Book
中找到了一个与之匹配的作者的所有MyItems
,然后从书籍列表中拉出所有Genre
,Publisher
等拉。通过这种方式,我可以根据作者提供的标签显示作者拥有的所有标签。 (我在列表视图模型的范围内进行此操作,因为我知道我要深入研究哪个项目并可以访问主MyItems
集合)
但是,使用这种方法,我似乎无法解决问题2。为了能够在Bookmarks.Count
上进行排序,我需要将其移至Base
并以某种方式针对每种相关标记类型进行填充。
如何在不给每种Book
或Author
访问全局项目集合的情况下,将这种信息公开给每种非Bookmark
标签类型(感觉像是一个很大的-否),还是维护每种标签类型的所有相关标签的列表(这简直让人感到效率低下)?
编辑1: 您可以定义“标签”和“标签类型”并给出更多示例吗?
我正在使用标记定义我想放入列表中的任何种类的商品。 Book
,Author
,Bookmark
都是标签。 Language
和Genre
也将是标签。一本书包含作者和语言,就像语言包含书籍和作者一样。
Edit2: 即使您不打算拥有一个备份数据库,也应该从重新学习实体关系模型知识中受益
我知道这是一个高度关系的结构。我确实有一个后备数据库,但是如何将其存储在数据库中与如何构造要绑定到ListCollectionView
答案 0 :(得分:4)
使用Book作为真理的来源不是一个好方法吗?
以当前的关系数据库模式为例,其中Book
以外的任何表都没有指向其他任何对象的外键引用。您始终必须浏览Book
表以查找包含书籍的任何关系。在您给出的示例中,您必须遍历整个书籍集,才能找到单个作者创作的所有书籍。如果您的参考文献朝另一个方向发展,则只需查找单个作者,然后查看其Books
属性。
您目前如何获得尚未写过任何书的作者名单?您必须扫描书籍列表,以获得没有拥有书籍的每个作者的列表,然后在该列表中找到每个 not 的作者。
我如何在不给每个作者或书签访问全局项目集合(感觉像是大禁忌)或维护每个项目的所有相关标签的列表的情况下,将这种信息公开给每种非Book标签类型标签类型(感觉确实效率很低)?
您将需要代表每个项目上每种标记类型的属性-确实没有办法解决。如果您希望根据列表中的书签数量对列表中的项目进行排序,那么每个项目都需要提供其书签的数量。
但是属性不必由预先计算的列表支持。它们可以有效地指导您如何进行适当的联接以获得所需的信息。例如,Bookmarks
的{{1}}属性将使用Author
属性来获取书签列表:
Books
如果需要,您还可以缓存结果。
如果您选择继续不让任何实体的引用返回到public IEnumerable<Bookmark> Bookmarks => this.Books.SelectMany(b => b.Bookmarks);
,而在模型类中使Book
可用,则可以对指向{{ 1}}。例如,在MyItems
中:
Book
不过,我不建议您这样做,因为您对它感觉不正确是正确的。它将模型的实现链接到单独的,不相关的数据结构。我的建议是实现与列表的直接关系,并对要排序的其他所有内容使用计算属性。
答案 1 :(得分:4)
我想我希望类型之间的关系尽可能地空灵。尽管大多数类型很容易关联,但有些类型具有复合键或奇数关系,而您却永远不知道...所以我将从这些类型本身对相关类型的发现进行外部化。我们中只有少数幸运的人具有全局唯一的一致密钥类型。
我可以想象让您所有的类型既是观察者又是可观察的。我从来没有大声地做过这样的事情...至少,不是这样,但这是一个有趣的可能性...并且给了500点,我认为值得一试;;)
我正在使用Tag
一词来关注您的评论。也许Base
对您来说更有意义?无论如何,在下文中,Tag
是一种通知观察标签并侦听可观察标签的类型。我将observables
设为Tag.Subscription
的列表。通常,您只需要列出IDisposable
个实例,因为通常可以提供所有这些。这样做的原因是Tag.Subscription
可以让您发现基础的Tag
...,以便您可以在派生类型中为类型的列表属性抓取订阅(如以下{{1}中所示) }和Author
。)
我建立了Book
订阅者/通知者机制,使其本身没有 values 就可以工作……只是为了隔离该机制。我假设大多数Tag
都有值...但是也许会有例外。
Tag
如果绝对所有标签都具有值,则可以将以下实现与前述实现合并...但是我认为将它们分开会更好。
public interface ITag : IObservable<ITag>, IObserver<ITag>, IDisposable
{
Type TagType { get; }
bool SubscribeToTag( ITag tag );
}
public class Tag : ITag
{
protected readonly List<Subscription> observables = new List<Subscription>( );
protected readonly List<IObserver<ITag>> observers = new List<IObserver<ITag>>( );
bool disposedValue = false;
protected Tag( ) { }
IDisposable IObservable<ITag>.Subscribe( IObserver<ITag> observer )
{
if ( !observers.Contains( observer ) )
{
observers.Add( observer );
observer.OnNext( this ); //--> or not...maybe you'd set some InitialSubscription state
//--> to help the observer distinguish initial notification from changes
}
return new Subscription( this, observer, observers );
}
public bool SubscribeToTag( ITag tag )
{
if ( observables.Any( subscription => subscription.Tag == tag ) ) return false; //--> could throw here
observables.Add( ( Subscription ) tag.Subscribe( this ) );
return true;
}
protected void Notify( ) => observers.ForEach( observer => observer.OnNext( this ) );
public virtual void OnNext( ITag value ) { }
public virtual void OnError( Exception error ) { }
public virtual void OnCompleted( ) { }
public Type TagType => GetType( );
protected virtual void Dispose( bool disposing )
{
if ( !disposedValue )
{
if ( disposing )
{
while ( observables.Count > 0 )
{
var sub = observables[ 0 ];
observables.RemoveAt( 0 );
( ( IDisposable ) sub ).Dispose( );
}
}
disposedValue = true;
}
}
public void Dispose( )
{
Dispose( true );
}
protected sealed class Subscription : IDisposable
{
readonly WeakReference<Tag> tag;
readonly List<IObserver<ITag>> observers;
readonly IObserver<ITag> observer;
internal Subscription( Tag tag, IObserver<ITag> observer, List<IObserver<ITag>> observers )
{
this.tag = new WeakReference<Tag>( tag );
this.observers = observers;
this.observer = observer;
}
void IDisposable.Dispose( )
{
if ( observers.Contains( observer ) ) observers.Remove( observer );
}
public Tag Tag
{
get
{
if ( tag.TryGetTarget( out Tag target ) )
{
return target;
}
return null;
}
}
}
}
虽然看起来有点忙,但主要是原始订阅机制和可处理性。派生类型将非常简单。
请注意受保护的public interface ITag<T> : ITag
{
T OriginalValue { get; }
T Value { get; set; }
bool IsReadOnly { get; }
}
public class Tag<T> : Tag, ITag<T>
{
T currentValue;
public Tag( T value, bool isReadOnly = true ) : base( )
{
IsReadOnly = isReadOnly;
OriginalValue = value;
currentValue = value;
}
public bool IsReadOnly { get; }
public T OriginalValue { get; }
public T Value
{
get
{
return currentValue;
}
set
{
if ( IsReadOnly ) throw new InvalidOperationException( "You should have checked!" );
if ( Value != null && !Value.Equals( value ) )
{
currentValue = value;
Notify( );
}
}
}
}
方法。我开始将其放入界面中,但意识到让外界可以访问它可能不是一个好主意。
所以...举个例子;这是一个示例Notify()
。注意Author
是如何建立相互关系的。并非每种类型都具有这样的方法...但是它说明了这样做是多么容易:
AddBook
...和public class Author : Tag<string>
{
public Author( string name ) : base( name ) { }
public void AddBook( Book book )
{
SubscribeToTag( book );
book.SubscribeToTag( this );
}
public IEnumerable<Book> Books
{
get
{
return
observables
.Where( o => o.Tag is Book )
.Select( o => ( Book ) o.Tag );
}
}
public override void OnNext( ITag value )
{
switch ( value.TagType.Name )
{
case nameof( Book ):
Console.WriteLine( $"{( ( Book ) value ).CurrentValue} happened to {CurrentValue}" );
break;
}
}
}
将相似。关于相互关系的另一种想法;如果您意外地通过Book
和Book
定义了关系,则没有害处,没有犯规……因为订阅机制只是悄悄地跳过了重复项(我确定是测试过这种情况):>
Author
...最后,通过一点测试来查看它是否有效:
public class Book : Tag<string>
{
public Book( string name ) : base( name ) { }
public void AddAuthor( Author author )
{
SubscribeToTag( author );
author.SubscribeToTag( this );
}
public IEnumerable<Author> Authors
{
get
{
return
observables
.Where( o => o.Tag is Author )
.Select( o => ( Author ) o.Tag );
}
}
public override void OnNext( ITag value )
{
switch ( value.TagType.Name )
{
case nameof( Author ):
Console.WriteLine( $"{( ( Author ) value ).CurrentValue} happened to {CurrentValue}" );
break;
}
}
}
...吐出这个:
var book = new Book( "Pride and..." );
var author = new Author( "Jane Doe" );
book.AddAuthor( author );
Console.WriteLine( "\nbook's authors..." );
foreach ( var writer in book.Authors )
{
Console.WriteLine( writer.Value );
}
Console.WriteLine( "\nauthor's books..." );
foreach ( var tome in author.Books )
{
Console.WriteLine( tome.Value );
}
author.AddBook( book ); //--> maybe an error
Console.WriteLine( "\nbook's authors..." );
foreach ( var writer in book.Authors )
{
Console.WriteLine( writer.Value );
}
Console.WriteLine( "\nauthor's books..." );
foreach ( var tome in author.Books )
{
Console.WriteLine( tome.Value );
}
虽然我的列表属性为Jane Doe happened to Pride and...
Pride and... happened to Jane Doe
book's authors...
Jane Doe
author's books...
Pride and...
book's authors...
Jane Doe
author's books...
Pride and...
,但您可以使它们成为延迟加载的列表。您需要能够使列表的后备存储失效,但这可能会从您的可观察对象中自然流出。
所有这一切都有数百种方法。我试图不被带走。不知道...需要做一些测试才能弄清楚这是多么实用...但是思考确实很有趣。
编辑
我忘记说明...书签了。我猜书签的值是可更新的页码?像这样:
IEnumerable<T>
然后,public class Bookmark : Tag<int>
{
public Bookmark( Book book, int pageNumber ) : base( pageNumber, false )
{
SubscribeToTag( book );
book.SubscribeToTag( this );
}
public Book Book
{
get
{
return
observables
.Where( o => o.Tag is Book )
.Select( o => o.Tag as Book )
.FirstOrDefault( ); //--> could be .First( ) if you null-check book in ctor
}
}
}
可能具有Book
属性:
IEnumerable<Bookmark>
整洁的是,作者的书签就是他们的书的书签:
public class Book : Tag<string>
{
//--> omitted stuff... <--//
public IEnumerable<Bookmark> Bookmarks
{
get
{
return
observables
.Where( o => o.Tag is Bookmark )
.Select( o => ( Bookmark ) o.Tag );
}
}
//--> omitted stuff... <--//
}
对于牛来说,我让书签采用了关于建筑的书……只是为了说明另一种方法。根据需要混合和匹配;-)请注意,书签没有书籍列表...仅是一本书籍...因为它更适合模型。有趣的是,您可以从一个书签中解析所有书籍的书签:
public class Author : Tag<string>
{
//--> omitted stuff... <--//
public IEnumerable<Bookmark> Bookmarks => Books.SelectMany( b => b.Bookmarks );
//--> omitted stuff... <--//
}
...并轻松获得所有作者的书签:
var bookmarks = new List<Bookmark>( bookmark.Book.Bookmarks );
答案 2 :(得分:2)
在这种情况下,我会在书籍,作者甚至书签中使用Id。 书籍/作者之间的任何关系都可以通过具有作者ID的书籍和具有书籍ID的作者来捕获。它还保证书/作者将是唯一的。
您为什么感到需要让Book,Author和Bookmark类从同一个基类继承?您是否要使用共享功能?
对于您要寻找的功能,我想说一些扩展方法可能真的有用,例如
int GetWrittenBooks(this Author author)
{
//either query your persistent storage or look it up in memory
}
我要说,请确保您在类中不要添加太多功能。例如,您的Book课对可能的作者生日不承担任何责任。如果“作者”的生日在“作者”类中,则“书”不应访问作者的生日,也不应拥有“作者”,而只能访问作者。这本书的作者只是“感兴趣”,仅此而已。
作者也一样:例如,它与Book x第150页上的字母数量无关。这是这本书的责任,与作者无关。
tl; dr:Single responsibility principle / Separation of concerns。
答案 3 :(得分:0)
基本上,您可以使用标准实体模型方法来处理此问题,就像处理任何数据库一样,只是在您的数据库处于内存中的情况下。
public class Repository
{
public List<Book> GetBooksByAuthor(Author author) {} // books by author
public List<Author> GetAuthors() {}
// ...
}
这里的另一个优点是,您可以以更有效的方式实现每个查找。
如果以后想将其交换为数据库,则可以为Repository
引入接口。
每个实体都可以保留对存储库的引用,因此您可以在实体级别上提供别名以方便使用:
public class Author
{
private Repository repo;
public Author(Repository repo)
{
this.repo = repo;
}
// ...
public List<Book> Books => this.repo.GetBooksByAuthor(this);
}
根据数据完整性的重要性,您还可以选择其他形式的集合,这样可以防止对查询进行修改或将更改列表保存到每个实体等等。
如果它是一个内部库,则可以简化许多这样的考虑以简化代码。