DNN Blogs

Written for the Community, by the Community

Related Data in 2sxc - Part 2

Written By Jeremy Farrance
2023-11-21

Show the Badges already!!

You may have realized that we do not have a list of badges, but instead, a list of BadgesAssigned for which each item only contains a 1) a pointer to a Badge and 2) a pointer to a Person. So we can get our badges for output now with the following foreach loop:

  @foreach ( var badge in badges )
  {
    <span>
      @badge.Child("Badge").Picture("Image", settings: "Square", width: 54)
    </span>  
  }

Duckie

There, we did the thing. Done? Sorta. But what about...

Iterating through to an improvement or three

This works, but we can improve readability and (slightly) performance. LINQ has .Select(), which can project each element in to a new form. Though similar to its SQL namesake, its more powerful. Its a great way to return only the few fields you need from a content type with a lot of details. But in this case, we will return the whole object. So, we are going back now to the previous LINQ in Part 1, where we got our list of BadgesAssigned. With one simple change, we can have badges be an actual list of Badges:

  var bas = AsItems(App.Data["BadgesAssigned"]);
  var badges = bas
    .Where(ba => ba.Child("Person")
      .Id == currPerson.Id
    )
    .Select(b => b.Child("Badge"))
  ;

So .Select() combined with 2sxc's .Child() method lets us return IEnumerable<ITypedItem> of the content type, Badges. Perfect. Then the final output for this Person's Badges is

  @foreach ( var badge in badges )
  {
    <span>@badge.Picture("Image", 
        settings: "Square", 
        width: 54
      )
    </span>  
  }

Sometimes improvements come from learning new features and vice versa. Sometimes they come from reading the docs and trying to do things in different ways, including a few that don't work. I kept drifting back to the code above (in Part 1) where we are getting our assigned badges for the current user. It returns items based on the .Id field (formerly .EntityId), an integer that is unique for all items in this 2sxc instance. We could have done the same thing with Guids, but there is not much benefit:

  .Where(ba => ba.Child("Person").Guid == currPerson.Guid

But seeing the .Guid reminded me about objects being equal using ===. It performs a strict equality comparison by taking into account the type. And we don't access the .Id property (a slight perforance improvement maybe). So now our code would look like this:

  .Where(ba => ba.Child("Person") === currPerson

That does not work. Checking the docs again for ITypeItem I notice .Equals() is implemented. So now we can do the same thing and let 2sxc handle the particulars:

  var bas = AsItems(App.Data["BadgesAssigned"]);
  var badges = bas
    .Where(ba => ba.Child("Person").Equals(currPerson))
    .Select(b => b.Child("Badge"))
  ;

This does not work either. But I am inclined to believe this is a bug. In order to debug this, I added some code to show that the actual .GetType()s of both items; they don't match, and I think they should. I need to report this as an Issue (done). Update Jan 2024: the issue was verfied and fixed. You need to be running 2sxc v17.1.3 or higher to use .Equals() as expected.

We still have the working code above using .Id or .Guid to match and select our Badges. But working this through reminded me — Gru says, "Light bulb." — to go look at one of the all time great 2sxc conveniences, .Parents(). Take a look at how simple our code becomes:

  var badges = currPerson.Parents(type: "BadgesAssiged")
    .Select(b => b.Child("Badge"))
  ;

Stop, look again. How did we get down to 2 lines? We just got rid of getting the list of BadgesAssigned and the .Where() is gone. Lets translate the highlighted code from left to right.

  • we start with currPerson
  • we call .Parents() telling it what (content) type to use
  • and it returns a list of all the items in BadgesAssigned for currPerson

As you can see by the next line of code (above, after the highlight), this returns the exact same thing we had previously, a list of the records in BadgesAssigned only for the current Person and then we continue on, projecting that using .Select() to a list of just Badges.

If you find yourself surprised by .Parents() and not familiar with, or understanding what it does, the answer is in the code it replaced, but we can explain it. We know BadgesAssigned has a field named Person, a pointer (link) to a record (an item) in the Person table (content type). To rephrase, BadgesAssigned is the parent and Person is the child. So if reversed, from Person's (child) point of view, we want all the (parent) records in BadgesAssigned.

Have a look at our complete piece of code. We are doing some fairly complex data retrieval in a readable way.

@inherits Custom.Hybrid.RazorTyped

@{
  var activePersons = AsItems(App.Data["Person"])
    .Where(p => p.Bool("Active"))
  ;

  @* 1. if the URL params provide a person ID, get that Person *@
  var currPerson = activePersons 
    .Where(p => p.Id == MyPage.Parameters.Int("person"))
    .FirstOrDefault()
  ;
  if ( currPerson == null ) {
    currPerson = activePersons.FirstOrDefault();
  }

  @* 2. for the selected person, get their list of assigned badges *@
  var badges = currPerson.Parents(type: "BadgesAssiged")
    .Select(b => b.Child("Badge"))
  ;
}

<div class="mb-3">
  @foreach ( var badge in badges )
  {
    <span>
      @badge.Picture("Image", 
        settings: "Square",
        width: 54,
        imgAlt: badge.String("Name"))
    </span>  
  }
</div>

So that is it. The journey is over. What did we just accomplish and learn?

  • We worked in the world of the new "Typed" 2sxc
  • Demonstrated database concepts like many-to-many joins work very naturally in 2sxc
  • Iterated the code a little, improving readability and performance
  • Related data doesn't have to have a link field in the content type; the data can exist elsewhere and be accessed easily (and elegantly)
  • We saw at least 2 great examples of why "Typed" is better (compare the old code below with what we wrote in Part 1)
  • We saw and talked through examples that used fairly advanced methods like .Child() and .Parents() that allowed us to greatly simplify complex, multi-step work in a pleasantly readable way
  • Without talking about it we used .Picture() to size and shape our images for perfect output using the modern HTML picture element (and the images got created and cached for reuse)
An Addendum: Answers to Why

Why didn't we just add a field to the Person content type named Badges as an Entity type, pointing to (multiple) Badges?

In this case, since this was for a large organization with around 1,100 employees and previous data showed that with turnover across 12 years there were 1,200 more records in the data for past (non-active) employees. And our Person content type for this client has 38 fields so far (we had a requirement to keep all fields from the old system, even ones we weren't using).

So adding another field would have both storage and performance implications. And since Badges were only being used in the employee profile page/view (and not in any of the other places staff members appeared across pages, departments, services, locations, and what not), being able to store the data in an external table without any hard coding issues to solve or any real performance impact, 2sxc made it an easy choice to store things like this.

One more note on storage that is specific to 2sxc that was sort-of the tipping point here. 2sxc keeps history on all item changes. So if the Person content type had the Badges field, every time you add or remove a badge and re-save the Person, you get another history item. Since the user stories and timelines had 100s of Person records adding and removing dozens of Badges at least quarterly, each staff member's Person record could have dozens or hundreds of unseen history copies being stored. Putting these Badge assignments in a separate content-type will greatly reduce this hidden storage cost.

Post-Addendum: Those two asterisks from Part 1

** even without Typed, this code could be improved, but its still a good example of a) handling all the possibilities and worse, b) it getting written over and over again differently across the years with varying degrees of success. While reviewing code from previous projects for this post, I quickly found over ten examples of doing this, and no two were similar.

Think about that from a productivity standpoint as a business owner; over the years, 3+ developers were presented with almost identical needs, and each time they chose to figure it out, write code as they thought through the problem and what exceptions to handle (or not). So not including the using statement(s), here is an example of getting a single item from a content type based on a URL param (circa 2019) and left the formatting as-is to highlight the readability problem(s):

  // see if URL has an Issuu EntityId to pass in
  var issuu = Content;
  int detailEntityId;
  if(int.TryParse(Request.QueryString["detail"], out detailEntityId)) {
    var issuus = AsDynamic(App.Data["ContentIssuu"]).Where(i => i.EntityId==detailEntityId);
    if(issuus.Count() == 1) {
      issuu = issuus.FirstOrDefault();
    }
  }

It's not too different, but the main if has no else, issuu is still pointing to the Content item. What is that? What if it's null? Or worse, what if it's an unexpected record or 2sxc's DemoItem? Also, detailEntityId was declared and so might be zero (the default if it fails). So, if subsequent code assumed it had the EntityID from the URL, that is not so, and (for example) links to edit the item would be broken.

And finally, another creative highlight from the recent past attempting the same solution:

  dynamic selectedOrg = null;
  if(!String.IsNullOrEmpty(Request.QueryString["org"])) {
    selectedOrg = AsList(App.Data["Organization"])
      .Where(x => x.EntityId == Int32.Parse(Request.QueryString["org"])).FirstOrDefault();
  }

Thanks to the following people for feedback that greatly improved article: Steve Karpik, Brittani Musgrove, and Jared Therrien.

PS, Dnn Platform, 2sxc, and most other things mentioned are all open source projects by developers who contribute their time without getting paid. These developers need to be supported for their efforts. If you benefit from open source and use (and love) these projects, I recommend you find out who the active developers are and consider Sponsoring them. GitHub has made this simple and you can read more here. The folks that I thanked below are all open source developers that work directly on the products mentioned in this article. I linked directly to their GitHub Sponsor pages if available.

Thanks for the great open source contributions: Daniel Mettler, David Poindexter, and Daniel Valadas

Read more of my Dnn Community Blog posts... More about Accuraty Solutions...

Total: 1 Comment(s)
Thanks Jeremy for sharing!
Tuesday, November 21, 2023 ·

Would you like to help us?

Awesome! Simply post in the forums using the link below and we'll get you started.

Get Involved