DynamicData:TransformToTree

时间:2017-05-28 18:49:24

标签: c# wpf system.reactive dynamic-data reactiveui

我有一个应用程序,可以使用反斜杠(\)字符在层次结构中构建标记。

例如

;

Country\Canada\Alberta
Country\Canada\British Columbia
Country\USA\California
Country\USA\Texas

将成为用户界面;

Country
    Canada
        Alberta
        British Columbia
    USA
        California
        Texas

在数据库中,它以字符串形式存储,并以TagDto的形式返回给客户端。我已经尝试了以下方法来实现这一目标;

public class TagDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class TagLeaf
{
    public string Id { get; }
    public string ParentId { get; }
    public int TagId { get; }
    public string Name { get; }

    public TagLeaf(string id, string parentId, int tagId, string name)
    {
        Id = id;
        ParentId = parentId;
        TagId = tagId;
        Name = name;
    }

    // IEquatable implemented on Id property.
}

public class TagsViewModel : ReactiveObject
{
    private IDisposable TagsSubscription { get; }

    public SourceCache<TagDto, string> Tags { get } = new SourceCache<TagDto, string>(t => t.Id);

    private readonly ReadOnlyObservableCollection<TagLeafViewModel> _tagTree;
    public ReadOnlyObservableCollection<TagLeafViewModel> TagTree => _tagTree;

    public ReactiveCommand AddBelgium { get; }

    public TagsViewModel()
    {
        AddBelgium = ReactiveCommand.Create(() => 
            Tags.AddOrUpdate(new TagDto {Id = 5, Name = @"Country\Belgium"});

        // this comes from an web service normally. 
        Tags.AddOrUpdate(new[] {
            new TagDto {Id = 1, Name = @"Country\Canada\Alberta"},
            new TagDto {Id = 2, Name = @"Country\Canada\British Columbia"},
            new TagDto {Id = 3, Name = @"Country\USA\California"},
            new TagDto {Id = 4, Name = @"Country\USA\Texas"}
        });

        TagsSubscription = Tags
            .Connect()
            .TransformMany(dto => 
            {
                var names = dto.Name.Split(new[] {'\\'}, StringSplitOptions.RemoveEmptyEntries);
                var results = new TagLeaf[names.Length];
                var parentId = "";
                for (var i = 0; i < names.Length; i++)
                {
                    var name = names[i];
                    var id = $"{parentId}{name}\\";
                    results[i] = new TagLeaf(id, parentId, dto.Id, name);
                    parentId = id;
                }

                return results;
            }, leaf => leaf.Id)
            .TransformToTree(leaf => leaf.ParentId)
            .Transform(leaf => new TagLeafViewModel(leaf))
            .Sort(SortExpressionComparer<TagLeafViewModel>.Ascending(vm => vm.Name))
            .Bind(out _tagTree)
            .Subscribe();
    }
}

public class TagLeafViewModel : ReactiveObject
{
    private readonly ReadOnlyObservableCollection<TagLeafViewModel> _children;
    public ReadOnlyObservableCollection<TagLeafViewModel> Children => _children;

    private string _name;
    public string Name
    {
        get => _name;
        set => this.RaiseAndSetIfChanged(ref _name, value);
    }

    public TagLeafViewModel(Node<TagLeaf, string> node)
    {
        Name = node.Item.Name;
        ChildrenSubscription = node.Children
            .Connect()
            .Transform(n => new TagLeafViewModel(n))
            .Sort(SortExpressionComparer<TagLeafViewModel>.Ascending(vm => vm.Name))
            .Bind(out _children)
            .Subscribe();
    }
}

// TagsView.xaml
<StackPanel>
    <Button x:Name="AddBelgiumButton" Content="Add Belgium"/>
    <telerik:RadTreeView x:Name="TagTreeView">
        <telerik:RadTreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Children}">
                <TextBlock Text="{Binding Name}"/>
            </HierarchicalDataTemplate>
        </telerik:RadTreeView.ItemTemplate>
    </telerik:RadtreeView>
</StackPanel>

// TagsView.xaml.cs constructor
public TagsView()
{
    ...
    this.WhenActivated(d => 
    {
        d(this.AddBelgiumButton.Events().Click.Select(x => Unit.Default).InvokeCommand(ViewModel, vm => vm.AddBelgium));
        d(this.OneWayBind(ViewModel, vm => vm.TagTree, v => v.TagTreeView.ItemsSource));
    });
}

这会产生一棵树,正如我所期望的那样,但如果我展开Country并点击添加比利时,而不是将此插入作为国家/地区下方的新节点插入树中 - 它会崩溃整个国家的节点。

添加新标记会导致2个新的TagLeaf流式传输到TramsformToTree。一个用于国家,一个用于比利时,所以我理解为什么它更新了国家节点但是,我不确定如何克服这个问题 - 任何建议都会非常感激。

1 个答案:

答案 0 :(得分:0)

我相信我已经取得了突破,但建议仍然受到欢迎。

在我之前的尝试中意识到TransformMany是问题所在,我认为有必要维护2个单独的缓存来实现我的目标。

我现在有一个TagService,它暴露了两个缓存。每当在基础TagDto缓存中更改项目时,我都会使用更改手动更新TagLeaf缓存。在我的示例应用程序中,现在插入新节点而不折叠根节点。

这是不完整的,我仍然需要处理在TagLeaf缓存中没有子项时删除父TagLeaf,但我相信我可以做到这一点,所以我认为问题已经解决了。

public class TagService : ITagService
{
    private readonly SourceCache<TagDto, int> _tagDtos = new SourceCache<TagDto, int>(t => t.Id);
    public IObservableCache<TagDto, int> TagDtos => _tagDtos;

    private readonly SourceCache<TagLeaf, string> _tagLeafs = new SourceCache<TagLeaf, string>(t => t.Id);
    public IObservableCache<TagLeaf, string> TagLeafs => _tagLeafs;

    public TagService()
    {
        _tagDtos.AddOrUpdate(new[]
        {
            new TagDto {Id = 1, Name = @"Country\Canada\Alberta"},
            new TagDto {Id = 2, Name = @"Country\Canada\British Columbia"},
            new TagDto {Id = 3, Name = @"Country\USA\California"},
            new TagDto {Id = 4, Name = @"Country\USA\Texas"}
        });

        _tagDtos
            .Connect()
            .Transform(dto =>
            {
                var names = dto.Name.Split(new[] {'\\'}, StringSplitOptions.RemoveEmptyEntries);
                var results = new TagLeaf[names.Length];

                var parentId = "";
                for (var i = 0; i < names.Length; i++)
                {
                    var name = names[i];
                    var id = $"{parentId}{name}\\";
                    results[i] = new TagLeaf(id, parentId, dto.Id, name);
                    parentId = id;
                }

                return new TagBranch(dto.Id, results);
            })
            .ForEachChange(change =>
            {
                var branch = change.Current;
                switch (change.Reason)
                {
                    case ChangeReason.Remove:
                        var lastLeaf = branch.Leaves.Last();
                        _tagLeafs.RemoveKey(lastLeaf.Id);
                        break;

                    case ChangeReason.Add:
                        foreach (var leaf in branch.Leaves)
                        {
                            if (_tagLeafs.Keys.Contains(leaf.Id))
                                continue;

                            _tagLeafs.AddOrUpdate(leaf);
                        }
                        break;
                }
            })
            .Subscribe();
    }

    public void AddOrUpdate(TagDto dto)
    {
        _tagDtos.AddOrUpdate(dto);
    }
}

TagsViewModel中的构造函数现在看起来像这样;

public TagsViewModel(ITagService tagService)
{
    AddBelgium = ReactiveCommand.Create(() =>
        tagService.AddOrUpdate(new TagDto {Id = 5, Name = @"Country\Belgium"}));

    TagsSubscription = tagService.TagLeafs
        .Connect()
        .TransformToTree(leaf => leaf.ParentId)
        .Transform(node => new TagLeafViewModel(node))
        .Sort(SortExpressionComparer<TagLeafViewModel>.Ascending(vm => vm.Name))
        .Bind(out _tagTree)
        .Subscribe();
}