App


Dynamic DataSources Tutorials

Tutorial HomeDynamic DataSources
#3 Custom Dynamic DataSources - Data Relationships
The samples can differ based on your Razor base class or if you're running an old version.
Selected: Typed (2sxc 16+) Switch to Dynamic (Razor14 or below)

Custom Dynamic DataSources - Data Relationships

Data can have relationships to other data, allowith code to navigate from one to the other.

The TreeBasic DataSource creates a list of entities which point to the children. This is a simple example how to establish relationships between generated data.

In this example, every entity has a SubItems property, which contains the IDs of the related children. IDs are a common way to declare relationships, but there are other options as well.

⬇️ Result | Source ➡️

Example Reading some Data

  • Root name: Root Node
  • Root ID: 1
  • Item count in field SubItems: 1
  • Title of first child: Sub Item 101
  • Looping through children of Root
    • Sub Item 101

Show Tree of Data

  • Item: Root Node (id: 1)
    • Sub-Items on field SubItems - found 2
      • Item: Sub Item 101 (id: 101)
        • Sub-Items on field SubItems - found 2
          • Item: Sub Item 1011 (id: 1011)
          • Item: Sub Item 1012 (id: 1012)
      • Item: Sub Item 102 (id: 102)

Flat List of Items 5

  • Root Node (#1)
  • Sub Item 101 (#101)
  • Sub Item 102 (#102)
  • Sub Item 1011 (#1011)
  • Sub Item 1012 (#1012)
@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade;
@using System.Linq;
@using ToSic.Eav.DataSources;

@{
  var tree = Kit.Data.GetSource(name: "TreeBasic");

  var root = AsItem(tree.List.FirstOrDefault(i => i.EntityId == 1));

  var rootSubItems = AsItems(root.Child("SubItems"));
}
<h3>Example Reading some Data</h3>
<ul>
  <li>Root name: @root.Title</li>
  <li>Root ID: @root.Id</li>
  <li>Item count in <em>field</em> <code>SubItems</code>: @rootSubItems.Count()</li>
  <li>Title of first child: @rootSubItems.First().Title</li>
  <li>
    Looping through children of Root
    <ul>
      @foreach (var child in AsItems(root.Child("SubItems"))) {
        <li>@child.Title</li>
      }
    </ul>
  </li>
</ul>

@* TODO:: Typed model  *@
@Html.Partial("../../data-sources/helpers/Show Tree.Typed.cshtml", 
  new { Title = "Show Tree of Data", Root = root, SubNodeNames = "SubItems" })

@Html.Partial("../../data-sources/helpers/Show Data List.Typed.cshtml", 
  new { Title = "Flat List of Items", Data = tree })

Source Code of TreeBasic.cs

using System.Collections.Generic;

public class TreeBasic : Custom.DataSource.DataSource16
{
  public TreeBasic(MyServices services) : base(services, "My.Magic")
  {
    ProvideOut(() => {     
      return new List<object> {
        // Root has ID 1 and points to 2 children
        CreateNode(1, "Root Node", new [] { 101, 102 }),
        // This is a subnode, with 2 more children
        CreateNode(101, "Sub Item 101", new [] { 1011, 1012 }),
        CreateNode(102, "Sub Item 102"),
        CreateNode(1011, "Sub Item 1011"),
        CreateNode(1012, "Sub Item 1012"),
      };
    });
  }

  private object CreateNode(int id, string title, int[] relationships = null) {
    return new {
      Id = id,
      Title = title,
      // To reference another item in the same list,
      // create an anonymous object with "Relationships" as an Enumerable, Array or List
      SubItems = new { Relationships = relationships }
    };
  }
}

The previous example only allowed navigating down from parent to children. To allow the children to know the parent, we can also provide that relationship.

⬇️ Result | Source ➡️

Show Tree of Data

  • Item: Root Node (id: 1)
    • Sub-Items on field SubItems - found 2
      • Item: Sub Item 101 (id: 101)
        Parent (field Parent): Root Node (id: 1)
        • Sub-Items on field SubItems - found 2
          • Item: Sub Item 1011 (id: 1011)
            Parent (field Parent): Sub Item 101 (id: 101)
          • Item: Sub Item 1012 (id: 1012)
            Parent (field Parent): Sub Item 101 (id: 101)
      • Item: Sub Item 102 (id: 102)
        Parent (field Parent): Root Node (id: 1)

Flat List of Items 5

  • Root Node (#1)
  • Sub Item 101 (#101)
  • Sub Item 102 (#102)
  • Sub Item 1011 (#1011)
  • Sub Item 1012 (#1012)
@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade;
@using System.Linq;
@using ToSic.Eav.DataSources;

@{
  var tree = Kit.Data.GetSource(name: "TreeBasicWithParents");
  var root = AsItem(tree.List.FirstOrDefault(i => i.EntityId == 1));
}
@Html.Partial("../../data-sources/helpers/Show Tree.Typed.cshtml",
  new { Title = "Show Tree of Data", Root = root, SubNodeNames = "SubItems", ParentField = "Parent" })

@Html.Partial("../../data-sources/helpers/Show Data List.Typed.cshtml", 
  new { Title = "Flat List of Items", Data = tree })

Source Code of TreeBasicWithParents.cs

using System.Collections.Generic;

public class TreeBasicWithParents : Custom.DataSource.DataSource16
{
  public TreeBasicWithParents(MyServices services) : base(services, "My.Magic")
  {
    ProvideOut(() => {     
      return new List<object> {
        // Root has ID 1 and points to 2 children
        CreateNode(1, "Root Node", 0, new [] { 101, 102 }),
        // This is a subnode, with 2 more children
        CreateNode(101, "Sub Item 101", 1, new [] { 1011, 1012 }),
        CreateNode(102, "Sub Item 102", 1),
        CreateNode(1011, "Sub Item 1011", 101),
        CreateNode(1012, "Sub Item 1012", 101),
      };
    });
  }

  private object CreateNode(int id, string title, int parent, int[] relationships = null) {
    return new {
      Id = id,
      Title = title,
      // To reference another item in the same list,
      // create an anonymous object with "Relationships" as an Enumerable, Array or List
      SubItems = new { Relationships = relationships },
      Parent = new { Relationships = parent }
    };
  }
}

The previous example used the Id number of items to establish a relationship. But there are many cases, where no ID-number exists. For example, data coming from a WebService or files might just have a string identifier.

The following example establishes the relationship based on a Path string:

  • Each item has a Path which is a reliably identifier
  • To tell the relationship manager that we have more keys, we must add a RelationshipKeys property
  • The RelationshipKeys can have many keys for advanced reasons, so it's an array
  • In this example, we just add the Path to the list of keys
  • In this example, the parent supplies a list of paths which it expects to have as children

⬇️ Result | Source ➡️

Show Tree of Data

  • Item: Root Node (id: 1)
    • Sub-Items on field SubItems - found 2
      • Item: Sub Item 101 (id: 2)
        • Sub-Items on field SubItems - found 2
          • Item: Sub Item 1011 (id: 4)
          • Item: Sub Item 1012 (id: 5)
      • Item: Sub Item 102 (id: 3)

Flat List of Items 5

  • Root Node (#1)
  • Sub Item 101 (#2)
  • Sub Item 102 (#3)
  • Sub Item 1011 (#4)
  • Sub Item 1012 (#5)
@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade;
@using System.Linq;
@using ToSic.Eav.DataSources;

@{
  var tree = Kit.Data.GetSource(name: "TreeChildPaths");
  var root = AsItem(tree.List.FirstOrDefault(i => i.EntityId == 1));
}

@Html.Partial("../../data-sources/helpers/Show Tree.Typed.cshtml",
  new { Title = "Show Tree of Data", Root = root, SubNodeNames = "SubItems", ParentField = "Parent" })

@Html.Partial("../../data-sources/helpers/Show Data List.Typed.cshtml", 
  new { Title = "Flat List of Items", Data = tree })

Source Code of TreeChildPaths.cs

using System.Collections.Generic;

public class TreeChildPaths : Custom.DataSource.DataSource16
{
  public TreeChildPaths(MyServices services) : base(services, "My.Magic")
  {
    ProvideOut(() => {
      return new List<object> {
        // Root has path "/" and points to 2 children
        CreateNode("/", "Root Node", new [] { "/101", "/102" }),
        // This is a subnode, with 2 more children
        CreateNode("/101", "Sub Item 101", new [] { "/101/1011", "/101/1012" }),
        CreateNode("/102", "Sub Item 102", null),
        CreateNode("/101/1011", "Sub Item 1011", null),
        CreateNode("/101/1012", "Sub Item 1012", null),
      };
    });
  }


  private object CreateNode(string path, string title, string[] relationships = null) {
    return new {
      Title = title,
      Path = path,
      // To reference another item in the same list,
      // create an anonymous object with "Relationships" as an Enumerable, Array or List
      SubItems = new { Relationships = relationships },
      RelationshipKeys = new [] { path },
    };
  }
}

The previous example used the Path of children to establish a relationship. But in real life, it's often the other way around. Usually the children know what parent they belong to. The following example establishes the relationship based on a Parent Path string:

  • In this example, we just add the parent path to the list of keys
  • Since the own path is used to establish the relationship, it will find all items which have the own path as a relationship key

⬇️ Result | Source ➡️

Show Tree of Data

  • Item: Root Node (id: 1)
    • Sub-Items on field SubItems - found 2
      • Item: Sub Item 101 (id: 2)
        • Sub-Items on field SubItems - found 2
          • Item: Sub Item 1011 (id: 4)
          • Item: Sub Item 1012 (id: 5)
      • Item: Sub Item 102 (id: 3)

Flat List of Items 5

  • Root Node (#1)
  • Sub Item 101 (#2)
  • Sub Item 102 (#3)
  • Sub Item 1011 (#4)
  • Sub Item 1012 (#5)
@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade;
@using System.Linq;
@using ToSic.Eav.DataSources;

@{
  var tree = Kit.Data.GetSource(name: "TreeParentPaths");
  var root = AsItem(tree.List.FirstOrDefault(i => i.EntityId == 1));
}

@Html.Partial("../../data-sources/helpers/Show Tree.Typed.cshtml",
  new { Title = "Show Tree of Data", Root = root, SubNodeNames = "SubItems", ParentField = "Parent" })

@Html.Partial("../../data-sources/helpers/Show Data List.Typed.cshtml", 
  new { Title = "Flat List of Items", Data = tree })

Source Code of TreeParentPaths.cs

using System.Collections.Generic;

public class TreeParentPaths : Custom.DataSource.DataSource16
{
  public TreeParentPaths(MyServices services) : base(services, "My.Magic")
  {
    ProvideOut(() => {
      return new List<object> {
        // Root has ID 1 and points to 2 children
        CreateNode("/", "Root Node"),
        // This is a subnode, with 2 more children
        CreateNode("/101", "Sub Item 101", "/"),
        CreateNode("/102", "Sub Item 102", "/"),
        CreateNode("/101/1011", "Sub Item 1011", "/101"),
        CreateNode("/101/1012", "Sub Item 1012", "/101"),
      };
    });
  }


  private object CreateNode(string path, string title, string parent = null) {
    return new {
      Title = title,
      Path = path,
      // This says that the sub-items all use the key
      // of the current item because they point to the parent
      // so the child points to the parent, not the parent to the child
      SubItems = new { Relationships = path },
      RelationshipKeys = new [] { parent },
    };
  }
}

This is a very advanced example, where we create different types of data on different streams and establish relationships.

  • The folders and files all just know about their own data, and about the parent
  • Folders find their subfolders by asking for all items having a key like folder-in:/101
  • Folders find their subfiles by asking for all items having a key like file-in:/101
  • Folders and files find their parent by asking for all (one expected) items having a key like folder:/

⬇️ Result | Source ➡️

Show Tree of Data

  • Item: Folder '/' (id: 1)
    Parent (field Parent): Folder '/' (id: 1)
    • Sub-Items on field Folders - found 2
      • Item: Folder '/101' (id: 2)
        Parent (field Parent): Folder '/101' (id: 2)
        • Sub-Items on field Folders - found 2
          • Item: Folder '/101/1011' (id: 3)
            Parent (field Parent): Folder '/101/1011' (id: 3)
          • Item: Folder '/101/1012' (id: 4)
            Parent (field Parent): Folder '/101/1012' (id: 4)
        • Sub-Items on field Files - found 1
          • Item: File Text in 101.txt (id: 5)
            Parent (field Parent): Folder '/101' (id: 2)
      • Item: Folder '/102' (id: 6)
        Parent (field Parent): Folder '/102' (id: 6)
    • Sub-Items on field Files - found 2
      • Item: File Test.txt (id: 7)
        Parent (field Parent): Folder '/' (id: 1)
      • Item: File Image.jpg (id: 8)
        Parent (field Parent): Folder '/' (id: 1)

Flat List of All Items 8

  • Folder '/' (#1)
  • Folder '/101' (#2)
  • Folder '/101/1011' (#3)
  • Folder '/101/1012' (#4)
  • File Text in 101.txt (#5)
  • Folder '/102' (#6)
  • File Test.txt (#7)
  • File Image.jpg (#8)

Flat List of Folders 5

  • Folder '/' (#1)
  • Folder '/101' (#2)
  • Folder '/101/1011' (#3)
  • Folder '/101/1012' (#4)
  • Folder '/102' (#6)

Flat List of Files 5

  • Folder '/' (#1)
  • Folder '/101' (#2)
  • Folder '/101/1011' (#3)
  • Folder '/101/1012' (#4)
  • Folder '/102' (#6)
@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade;
@using System.Linq;
@using ToSic.Eav.DataSources;

@{
  var tree = Kit.Data.GetSource(name: "TreeFoldersAndFiles");
  var root = AsItem(tree.List.FirstOrDefault(i => i.EntityId == 1));
}

@Html.Partial("../../data-sources/helpers/Show Tree.Typed.cshtml",
  new { Title = "Show Tree of Data", Root = root, SubNodeNames = "Folders,Files", ParentField = "Parent" })

@Html.Partial("../../data-sources/helpers/Show Data List.Typed.cshtml", 
  new { Title = "Flat List of All Items", Data = tree })

@Html.Partial("../../data-sources/helpers/Show Data List.Typed.cshtml", 
  new { Title = "Flat List of Folders", Data = tree["Folders"] })

@Html.Partial("../../data-sources/helpers/Show Data List.Typed.cshtml", 
  new { Title = "Flat List of Files", Data = tree["Folders"] })

Source Code of TreeFoldersAndFiles.cs

using System.Collections.Generic;
using System.Linq;

public class TreeFoldersAndFiles : Custom.DataSource.DataSource16
{
  public TreeFoldersAndFiles(MyServices services) : base(services)
  {
    // The "Default" stream contains both files and folders
    ProvideOut(() => {
      return new List<object> {
        CreateFolder("/", ""),            // Root Folder
          CreateFolder("/", "101"),       // Subfolder 101
            CreateFolder("/101", "1011"),
            CreateFolder("/101", "1012"),
            CreateFile("/101", "Text in 101.txt"),
          CreateFolder("/", "102"),       // Subfolder 102
          CreateFile("/", "Test.txt"),    // File in Root folder
          CreateFile("/", "Image.jpg"),   // File in Root Folder
      };
    });
    ProvideOut(() => TryGetOut("Default").Where(f => !f.Get<bool>("IsFile")), name: "Folders");
    ProvideOut(() => TryGetOut("Default").Where(f => f.Get<bool>("IsFile")), name: "Files");
  }


  private object CreateFile(string path, string name) {
    var parentPath = path.ToLowerInvariant();
    var fullPath = (parentPath + "/" + name).ToLowerInvariant();
    return new {
      IsFile = true,
      Path = fullPath,
      Title = "File " + name,

      // Parent is the entity (one expected) which has a key saying they are this folder
      Parent = new { Relationships = "folder:" + parentPath },

      // Declare keys for anything that wants a relationship to this file
      RelationshipKeys = new [] {
        "file:" + fullPath,       // things that explicitly need this file
        "file-in:" + parentPath   // the parent folder will look for all of its files
      },
    };
  }
  private object CreateFolder(string parent, string name) {
    parent = parent.ToLowerInvariant();
    var path = (parent + (parent.EndsWith("/") ? "" : "/") + name).ToLowerInvariant();
    var parentPath = (path == "/" ? "" : parent).ToLowerInvariant();
    return new {
      IsFile = false,
      Path = path,
      Title = "Folder '" + path + "'",
      // Files should list all files which have this folder as parent
      Files = new { Relationships = "file-in:" + path },
      // Folders should list all folders which have this folder as parent
      Folders = new { Relationships = "folder-in:" + path },
      // Parent should point to the folder which is the parent of this folder
      Parent = new { Relationships = "folder:" + path },

      // Declare keys for anything that wants a relationship to this folder
      RelationshipKeys = new [] {
        "folder:" + path,           // things that explicitly need this folder
        "folder-in:" + parentPath,  // the parent folder will look for all of its files
      },
    };
  }
}

This example will change how the automatic ID is generated, what the ContentTypeName is, and which field to use for the EntityTitle.

⬇️ Result | Source ➡️

Data in the DataSource (4)

  • Greeting from FactoryOptions (id: 1000, type: MyContentType/MyContentType - ToSic.Eav.Data.ContentType)
  • Greeting from FactoryOptions (id: 1001, type: MyContentType/MyContentType - ToSic.Eav.Data.ContentType)
  • Greeting from FactoryOptions (id: 1002, type: MyContentType/MyContentType - ToSic.Eav.Data.ContentType)
  • Greeting from FactoryOptions (id: 1003, type: MyContentType/MyContentType - ToSic.Eav.Data.ContentType)
@inherits Custom.Hybrid.RazorTyped
@using ToSic.Razor.Blade;
@using System.Linq;
@using ToSic.Eav.DataSources;
@{
  var dsWithOptions = Kit.Data.GetSource(name: "FactoryOptions");
}
<h3>Data in the DataSource (@dsWithOptions.List.Count())</h3>
<ul>
  @foreach (var item in AsItems(dsWithOptions)) {
    <li>
      <strong>@item.Title</strong> (id: @item.Id, type: @item.Type)
    </li>
  }
</ul>

 

#3 Custom Dynamic DataSources - Data Relationships