entity-frameworkdevexpressiqueryabletreelist

Combining multiple IQueryable from different object types for TreeList DataSource


I search for a way to combine two or more IQueryables from different Object types in order to use it as a datasource for my treelist.

For the treelist I use the DevExpress WinForms component "TreeList". It provides me the properties "KeyFieldName" which is usually mapped to the "ID" and the ParentFieldName which is mapped to the parent id in order to build a hierarchy.

I use entity framework 6 as or mapper.

I have the two following classes I would need to combine:

XObject:

[Table("tbl_objects")]
public class XObject
{
    [Column("id")]
    public int Id { get; set; }
    [Column("display_name")]
    public String DisplayName { get; set; }
    [Column("description")]
    public String Description { get; set; }
    [Column("usage_reason")]
    public String UsageReason { get; set; }
    [Column("is_network_compatible")]
    public bool IsNetworkCompatible { get; set; }
    [Column("ip_address")]
    public String IpAddress { get; set; }
    [Column("network_name")]
    public String NetworkName { get; set; }
    [Column("serial_number")]
    public String SerialNumber { get; set; }
    [Column("manufacturer_identification_code")]
    public String ManufacturerIdentificationCode { get; set; }
    [Column("web_link")]
    public String WebLink { get; set; }
    [Column("warranty")]
    public int WarrantyInDays { get; set; }

    [Column("ref_manufacturer")]
    public virtual XManufacturer Manufacturer { get; set; }
    [Column("ref_order")]
    public virtual XOrder Order { get; set; }
    [Column("ref_owner")]
    public virtual XOwner Owner { get; set; }
    [Column("ref_room")]
    public virtual XRoom Room { get; set; }
    [Column("ref_object_folder")]
    public virtual XObjectFolder ObjectFolder { get; set; }
    public virtual ICollection<XAdditionalObjectData> AdditionalObjectData { get; set; }
}

XObjectFolder:

[Table("tbl_object_folders")]
public class XObjectFolder
{
    [Column("id")]
    public int Id { get; set; }
    [Column("display_name")]
    public String DisplayName { get; set; }
    [Column("short_name")]
    public String ShortName { get; set; }
    [Column("ref_parent_folder")]
    public virtual XObjectFolder ParentFolder { get; set; }
    public virtual ICollection<XObjectFolder> ChildFolders { get; set; }
    public virtual ICollection<XObject> Objects { get; set; }

    [NotMapped]
    public int ParentFolderId { get { return ParentFolder == null ? -1 : ParentFolder.Id; } }
}

As you've probably already seen, an object folder can contain subfolders but also objects. My goal is to see this as one "datasource" in my treelist. For example like this:

In other questions here I've found the possibilities to concat or union queryables, but that only works with them being the same type:

        using (var db = new XDbContext(_conString))
        {
            // Queryables
            var ofs = from of in db.ObjectFolders orderby of.DisplayName ascending select of; // <- All ObjectFolders
            var obs = from obj in db.Objects orderby obj.DisplayName ascending select obj; // <- All Objects

            // Concat them
            var comb = ofs.Concat(obs); // <- not the same type

            // As DataSource for my TreeList
            TreeListObjects.DataSource = comb.ToList();
        }

Which is why I am searching for a good way to make this possible.

I could also imagine me using a pretty bad approach to reach my goal. So I am open to suggestions. This is a personal project which I do to improve myself at stuff.

Thanks in advance!

EDIT

So I managed to get a step further by using an interface both classes share:

public interface ITreeListCombinable
{
    int Id { get; set; }
    int ParentId { get; }
    String DisplayName { get; set; }
}

But... who would've thought... there occures another problem:

Have a look at the db structure: Db_Struture

Since both objects are stored in different tables, the id's will certainly not be unique when combining them. Which is necessary when setting the datasource.


Solution

  • Solution:

    So I've taken my own approach to my problem and it worked out.

    Full disclosure -> I consider myself a beginner, so this solution is probably not the best. Still, if anyone is in a similar situation, here's how it could work:

    First I created an interface, which both the folder and objects share:

    ITreeListCombinable

    public interface ITreeListCombinable
    {
        int Id { get; set; }
        int ParentId { get; }
        int ListId { get; set; }
        int ParentListId { get; set; }
        String DisplayName { get; set; }
        ObjectTreeListElementTypes TreeListElementType { get; }
    }
    

    I then made sure, both my XObject and XObjectFolder classes held the ObjectTreeListElementTypes value they're corresponding to:

    ObjectTreeListElementTypes Enum:

    public enum ObjectTreeListElementTypes
    { 
        Folder,
        Object
    }
    

    Classes:

    [NotMapped]
    public ObjectTreeListElementTypes TreeListElementType => ObjectTreeListElementTypes.Folder; // or *.Object for that matter
    

    So afterwards I've wrote my own "controller" which handles my specific scenario.

    ObjectTreeListElementController:

    public class ObjectTreeListElementController
    {
        private List<ITreeListCombinable> _list;
    
        public ObjectTreeListElementController()
        {
            _list = new List<ITreeListCombinable>();
        }
    
        public void AddRange(List<ITreeListCombinable> list)
        {
            // add incoming items to private _list
            _list.AddRange(list);
        }
    
        public List<ITreeListCombinable> GetDataSourceList()
        {
            // create auto increment list id
            var listId = 0;
            foreach (var item in _list)
            {
                item.ListId = listId;
                listId++;
            }
    
            // set new parent list id according to incremental list id
            foreach (var item in _list)
            {
                var parents = _list.Where(x => x.Id == item.ParentId && x.TreeListElementType == ObjectTreeListElementTypes.Folder);
                if (parents.Count() > 0)
                    item.ParentListId = parents.First().ListId;
                else
                    item.ParentListId = -1;
            }
    
                return _list;
        }
    }
    

    Essentially, when calling the GetDataSourceList() method, it firstly distributes incremental, temporary list-ids. In a second loop I then search for the original parent id and match the tree list element type. If none is found, this folder is a root folder in my treelist, if one is found, the given list-id becomes the parent list id:

            using (var db = new XDbContext(_conString))
            {
                // Queryables
                IQueryable<ITreeListCombinable> ofs = from of in db.ObjectFolders orderby of.DisplayName ascending select of;
                IQueryable<ITreeListCombinable> objs = from obj in db.Objects orderby obj.DisplayName ascending select obj;
    
                var lofs = ofs.ToList();
                var lobjs = objs.ToList();
    
                var ctrl = new ObjectTreeListElementController();
                ctrl.AddRange(lofs);
                ctrl.AddRange(lobjs);
                var sourceList = ctrl.GetDataSourceList();
    
                // As DataSource for my TreeList
                TreeListObjects.DataSource = sourceList;
            }
    

    And this brought me the exact output I've wanted: Result

    Hope this helps another beginner :)