将Dictionary<string, object>转换为DataTable

3

我遇到了一个非常奇怪的问题,不知道该如何解决。

我有一个 IEnumerable<Dictionary<string,object>>,它可以包含一个或多个 IEnumerable<Dictionary<string,object>>

现在,这个字典需要被导入到 DataTable 中。如果 IEnumberable<Dictionary<string,object>> 内部没有子项,则 DataTable 应该只包含一行,列名为字符串,RowData 为对象(在此情况下为字符串)。但是,如果存在子项,则 DataTable 应该包含与此子项相同数量的行,并且每行中都有来自父级的其他信息。

例如,父级字典具有以下值:

string, object
---------------
Name, Mike
LastName, Tyson

IEnumerable Dictionary 子项具有:

string, object
----------------
[0]
ChildName, John
ChildAge, 10
[1] ChildName, Tony ChildAge, 12

结果应该是:

Name    LastName     ChildName     ChildAge
--------------------------------------------
Mike    Tyson        John          10
Mike    Tyson        Tony          12

此外,Parent IEnumerable 可以有许多 children IEnumerable,但它们的大小都相同。

有人有解决方法吗?

static void Main(string[] args)
{
    var child1 = new List<Dictionary<string, object>>();
    var childOneDic = new Dictionary<string, object> 
    { 
        { "ChildName", "John" }, 
        { "ChildAge", 10 } 
    };
    child1.Add(childOneDic);

    var child2 = new List<Dictionary<string, object>>();
    var childTwoDic = new Dictionary<string, object> 
    { 
        { "ChildName", "Tony" }, 
        { "ChildAge", 12 } 
    };
    child2.Add(childTwoDic);

    var parrent = new List<Dictionary<string, object>>();
    var parrentDic = new Dictionary<string, object>
    {
        { "Name", "Mike" },
        { "LastName", "Tyson" },
        { "child1", child1 },
        { "child2", child2 }
    };
    parrent.Add(parrentDic);

    var table = new DataTable();
    table.Columns.Add("Name");
    table.Columns.Add("LastName");
    table.Columns.Add("ChildName");
    table.Columns.Add("ChildAge");
    table = CreateTable(parrent, null, table);
}

static DataTable CreateTable(IEnumerable<Dictionary<string, object>> parrent, 
                             DataRow row, DataTable table)
{
    if (row == null)
    {
        row = table.NewRow();
    }

    foreach (var v in parrent)
    {
        foreach (var o in v)
        {
            if (o.Value.GetType().IsGenericType)
            {
                var dic = (IEnumerable<Dictionary<string, object>>) o.Value;
                CreateTable(dic, row, table);
            }
            else
            {
                row[o.Key] = o.Value;
            }
        }
        if (row.RowState == DataRowState.Added)
        {
            DataRow tempRow = table.NewRow();
            tempRow.ItemArray = row.ItemArray;
            table.Rows.Add(tempRow);
        }
        else
        {
            table.Rows.Add(row);
        }
    }

    return table;
}

一旦你解决了单层的问题,假设你的堆栈不会溢出,那么递归地解决其他层级的问题不就可以了吗? - user645280
这似乎并不难。看起来可以做到。你目前尝试了什么?如果你被卡住了,我们会帮助你。 - Sriram Sakthivel
最好是为我们创建实际的数据,以便我们可以尝试一下。 - ΩmegaMan
1
我记得一些代码 - 今天已经花了6个小时在这上面,现在刚出门。现在我又在思考它。我想我还在递归函数中:D 顺便说一下:代码看起来并不完全像这样 - 这只是我现在脑海中的想法。 - Goran
这是我获取数据的方式 - 我无法做任何改变,而且最容易后续操作数据的方式是使用 DataTable。这就是为什么我正在尝试创建一个 DataTable。 - Goran
显示剩余3条评论
2个回答

4

Linq是这项工作的好选择。我仍然认为您应该重新考虑设计,这样做太可怕了。这应该可以做到(而且没有任何硬编码):

var child1 = new List<IDictionary<string, object>>
{
    new Dictionary<string, object> { { "ChildName", "John" }, { "ChildAge", 10 } }
};

var child2 = new List<IDictionary<string, object>>
{
    new Dictionary<string, object> { { "ChildName", "Tony" }, { "ChildAge", 12 } }
};

var parent = new List<IDictionary<string, object>>
{
    new Dictionary<string, object> 
    { 
        { "Name", "Mike" },
        { "LastName", "Tyson" },
        { "child1", child1 },
        { "child2", child2 }
    },
    new Dictionary<string, object>
    {
        { "Name", "Lykke" },
        { "LastName", "Li" },
        { "child1", child1 },
    },
    new Dictionary<string, object>
    { 
        { "Name", "Mike" },
        { "LastName", "Oldfield" }
    }
};

CreateTable(parent);

static DataTable CreateTable(IEnumerable<IDictionary<string, object>> parents)
{
    var table = new DataTable();

    foreach (var parent in parents)
    {
        var children = parent.Values
                             .OfType<IEnumerable<IDictionary<string, object>>>()
                             .ToArray();

        var length = children.Any() ? children.Length : 1;

        var parentEntries = parent.Where(x => x.Value is string)
                                  .Repeat(length)
                                  .ToLookup(x => x.Key, x => x.Value);
        var childEntries = children.SelectMany(x => x.First())
                                   .ToLookup(x => x.Key, x => x.Value);

        var allEntries = parentEntries.Concat(childEntries)
                                      .ToDictionary(x => x.Key, x => x.ToArray());

        var headers = allEntries.Select(x => x.Key)
                                .Except(table.Columns
                                             .Cast<DataColumn>()
                                             .Select(x => x.ColumnName))
                                .Select(x => new DataColumn(x))
                                .ToArray();
        table.Columns.AddRange(headers);

        var addedRows = new int[length];
        for (int i = 0; i < length; i++)
            addedRows[i] = table.Rows.IndexOf(table.Rows.Add());

        foreach (DataColumn col in table.Columns)
        {
            object[] columnRows;
            if (!allEntries.TryGetValue(col.ColumnName, out columnRows))
                continue;

            for (int i = 0; i < addedRows.Length; i++)
                table.Rows[addedRows[i]][col] = columnRows[i];
        }
    }

    return table;
}

这是我使用过的一个扩展方法:

public static IEnumerable<T> Repeat<T>(this IEnumerable<T> source, int times)
{
    source = source.ToArray();
    return Enumerable.Range(0, times).SelectMany(_ => source);
}

你可以更加符合习惯的方式创建addedRows变量(我更喜欢这种方式),但对于其他人来说可能会稍微难懂一些。可以用单行代码实现,像这样:
var addedRows = Enumerable.Range(0, length)
                          .Select(x => new
                          {
                              relativeIndex = x,
                              actualIndex = table.Rows.IndexOf(table.Rows.Add())
                          })
                          .ToArray();

这里的棘手之处在于正确地进行枢轴操作。在我们的情况下,使用索引器不是什么大问题。请使用一组示例进行测试,并让我知道是否存在错误。


另一种方法是预先计算标题(循环之前的数据表列),因为它不会改变。但这也意味着需要额外的枚举循环。哪种方法更有效,您需要进行测试。我认为第一种方法更优雅。

static DataTable CreateTable(IEnumerable<IDictionary<string, object>> parents)
{
    var table = new DataTable();

    //excuse the meaningless variable names

    var c = parents.FirstOrDefault(x => x.Values
                                         .OfType<IEnumerable<IDictionary<string, object>>>()
                                         .Any());
    var p = c ?? parents.FirstOrDefault();
    if (p == null)
        return table;

    var headers = p.Where(x => x.Value is string)
                   .Select(x => x.Key)
                   .Concat(c == null ? 
                           Enumerable.Empty<string>() : 
                           c.Values
                            .OfType<IEnumerable<IDictionary<string, object>>>()
                            .First()
                            .SelectMany(x => x.Keys))
                   .Select(x => new DataColumn(x))
                   .ToArray();
    table.Columns.AddRange(headers);

    foreach (var parent in parents)
    {
        var children = parent.Values
                             .OfType<IEnumerable<IDictionary<string, object>>>()
                             .ToArray();

        var length = children.Any() ? children.Length : 1;

        var parentEntries = parent.Where(x => x.Value is string)
                                  .Repeat(length)
                                  .ToLookup(x => x.Key, x => x.Value);
        var childEntries = children.SelectMany(x => x.First())
                                   .ToLookup(x => x.Key, x => x.Value);

        var allEntries = parentEntries.Concat(childEntries)
                                      .ToDictionary(x => x.Key, x => x.ToArray());

        var addedRows = Enumerable.Range(0, length)
                                  .Select(x => new
                                  {
                                      relativeIndex = x,
                                      actualIndex = table.Rows.IndexOf(table.Rows.Add())
                                  })
                                  .ToArray();

        foreach (DataColumn col in table.Columns)
        {
            object[] columnRows;
            if (!allEntries.TryGetValue(col.ColumnName, out columnRows))
                continue;

            foreach (var row in addedRows)
                table.Rows[row.actualIndex][col] = columnRows[row.relativeIndex];
        }
    }

    return table;
}

@GoranZadro 好的,我会更新我的答案,应该很简单。只是你需要适当地添加列标题。 - nawfal
@GoranZadro,“Name”和“LastName”是否保证在父字典中存在? - nawfal
实际上不是这样的,它可以是任何东西。 - Goran
@GoranZadro,不,我的问题是你是否保证一对值(在你的情况下是“Name”和“LastName”)存在于所有父字典中?无论如何,我会写出我的答案。看看它是否适合你的数据。无法想象所有可能的情况。 - nawfal
1
是的,它们会,并且这很完美地运作!伙计,你太棒了!非常感谢你 - 这真是一项非常出色的工作! - Goran
显示剩余5条评论

0
我建议您使用类,这样可以更轻松地查看和操作数据。以下是根据您提供的示例所能做的内容的示例。关键也在于在子项内保留父项的引用。这样您只需要将子列表传递给网格即可。
    static void Main()
    {
        var child1 = new List<Dictionary<string, object>>();
        var childOneDic = new Dictionary<string, object> 
{ 
    { "ChildName", "John" }, 
    { "ChildAge", 10 } 
};
        child1.Add(childOneDic);

        var child2 = new List<Dictionary<string, object>>();
        var childTwoDic = new Dictionary<string, object> 
{ 
    { "ChildName", "Tony" }, 
    { "ChildAge", 12 } 
};
        child2.Add(childTwoDic);

        var parrent = new List<Dictionary<string, object>>();
        var parrentDic = new Dictionary<string, object>
{
    { "Name", "Mike" },
    { "LastName", "Tyson" },
    { "child1", child1 },
    { "child2", child2 }
};
        parrent.Add(parrentDic);


        List<Parent> goodList = new List<Parent>();
        List<Child> allChilds = new List<Child>();

        foreach (Dictionary<string, object> p in parrent)
        {
            Parent newParent = new Parent(p);

            goodList.Add(newParent);
            allChilds.AddRange(newParent.Childs);
        }

        foreach (Child c in allChilds)
        {
            Console.WriteLine(c.ParentName + ":" + c.ParentName + ":" + c.Name + ":" + c.Age);
        }

        Console.ReadLine();

    }

    public class Parent
    {
        private List<Child> _childs = new List<Child>();

        private Dictionary<string, object> _dto;

        public Parent(Dictionary<string, object> dto)
        {
            _dto = dto;

            for (int i = 0; i <= 99; i++)
            {
                if (_dto.ContainsKey("child" + i))
                {
                    _childs.Add(new Child(((List<Dictionary<string, object>>)_dto["child" + i])[0], this));
                }
            }

        }

        public string Name
        {
            get { return (string)_dto["Name"]; }
        }

        public string LastName
        {
            get { return (string)_dto["LastName"]; }
        }

        public List<Child> Childs
        {
            get { return _childs; }

        }

    }

    public class Child
    {

        private Parent _parent;

        private Dictionary<string, object> _dto;

        public Child(Dictionary<string, object> dto, Parent parent)
        {
            _parent = parent;
            _dto = dto;

        }

        public string Name
        {
            get { return (string)_dto["ChildName"]; }
        }


        public int Age
        {
            get { return (int)_dto["ChildAge"]; }
        }

        public string ParentName
        {
            get { return _parent.Name; }
        }

        public string ParentLastName
        {
            get { return _parent.LastName; }
        }

    }
}

我不确定如何在不陷入同样的问题中完成这个任务。此外,我认为不能使用类,因为我需要一个非常通用的方法来处理多达n个子元素(或没有子元素),而且它们可能具有不同的属性。 - Goran
@GoranZadro 我修改了我的答案以反映你提供的示例。 - the_lotus
只有一个问题 - 我不明白什么意思: - Goran
只有一个问题 - 我不明白这一行代码的含义: _childs.Add(new Child(_dto"child" + i, this)); 出现了构建错误弹窗,所以我将其更改为 _childs.Add(new Child(_dto, this)); - 不确定这是否符合预期? - Goran
@GoranZadro 很抱歉,我已经进行了正确的更改,现在应该可以正常工作了。我将其从vb.net转换而来,但并没有进行测试。 - the_lotus
看起来不错,但是当我有没有子元素的父元素时,我只会从具有子元素的父对象中得到结果。我已经更新了示例代码以包括所有情况,现在结果应该有4行,两行为Mike Tyson,一行为Lykke Li和一行为Mike Oldfield,其中子列名称为空值。 - Goran

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接