Umbraco backoffice listview + infinite editing - part 1
Tags: umbraco
Published: 2021-03-24 21:31:45

_Hi my name is Jesper 👋_

_I've recently started a new job as an Umbraco developer at a company called Limbo. I recently had to learn how to create a custom listview that uses infinite editing and thought I would share._

This blogpost will show you how to create a custom listview in the Umbraco backoffice, for the example we will create a custom dashboard to show it in, but it could just as well be a content app / section / dialogue menu / anything else! Let's dig in!

# Step 1 - Creating a simple dashboard in the backoffice

To create our dashboard we need to create a few files inside a new folder. So first we create a folder called "CustomDashboard" inside App_Plugins. In that folder we create the following files:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h01xyqc9gtl7b02pnduv.png) 

If you are unfamiliar with this process - Umbraco automatically registers Backoffice extensions on startup - it runs through things in the App_Plugins folder, and everything registered in a package.manifest file will be added.

So for the dashboard to show up we need to fill out the package.manifest file, and then restart the site so it gets picked up!

Here is the initial content of each file:

{% details _package.manifest_ %} 
 
```javascript
{
    "dashboards":  [
        {
            "alias": "myCustomDashboard",
            "view":  "/App_Plugins/CustomDashboard/dashboard.html",
            "sections":  [ "content" ],
            "weight": 15
        }
    ],
    "javascript": [
        "~/App_Plugins/CustomDashboard/dashboard.controller.js"
  ]
}
```
_Note: We set the section to be content and the weight to 15, which will place it between the welcome dashboard and redirects. [Read more on default dashboard weights](https://our.umbraco.com/documentation/Extending/Dashboards/#re-ordering--weighting)._

{% enddetails %}

{% details _dashboard.html_ %} 

```html
<div ng-controller="MyDashboardController as vm">
    <h1>Welcome to my dashboard!</h1>
</div>
```

{% enddetails %}

{% details _dashboard.controller.js_ %} 

```js
(function () {
    "use strict";

    function DashboardController() {

    }

    angular.module("umbraco").controller("MyDashboardController", DashboardController);

})();
```
_Note: It is intentional I register the controller at the bottom here as "MyDashboardController", want to make sure it's not conflicting with any of Umbracos own namespaces!_

{% enddetails %}

{% details _lang/en-US.xml_ %} 

```xml
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<language>
  <area alias="dashboardTabs">
    <key alias="myCustomDashboard">Custom Dashboard</key>
  </area>
</language>
```
_Note: This language file is nessecary to show the dashboard name in the backoffice. Dashboard names are [localized](https://our.umbraco.com/Documentation/Extending/Language-Files/) by default, the key alias is the alias of the dashboard set in the package.manifest_

{% enddetails %}

After I generate and save all these files I will restart the site (adding a space to web.config and saving is a super quick way of doing this), and the dashboard is ready for us to start working on a listview:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j8ps88ej1w7vplp5s7ar.png)

# Step 2 - Getting data in your controller and output it

Alright, let's get some content to populate our listview with. In most cases like this you probably have some data from an external source or you may gather some data from Umbraco itself.

To make it fast and easy I will get a list of content that has a scheduled publish date within the coming week. I've created a Backoffice authorized API controller:

{% details _DashboardController.cs_ %}
```csharp
using System;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.WebApi;

namespace Backoffice.Controllers
{
    public class DashboardController : UmbracoAuthorizedApiController
    {
        private readonly IContentService _contentService;

        public DashboardController(IContentService contentService)
        {
            _contentService = contentService;
        }

        public IEnumerable<IContent> GetContentForReleaseNextWeek ()
        {
            var endDate = DateTime.Now.AddDays(7);

            return _contentService.GetContentForRelease(endDate);                
        }
    }
}
```

You can read more about [UmbracoAuthorizedApiControllers in the Umbraco docs](https://our.umbraco.com/Documentation/Reference/Routing/WebApi/#backoffice-controllers)
Only thing you need to know is it is automatically authorized from the backoffice, and not from elsewhere. It is also automatically routed to `~/Umbraco/backoffice/Dashboard/GetContentForReleaseNextWeek`

If you are in doubt about where to put your C# controller, I would recommend a seperate Class library that is compiled into a dll for your website. Here is my setup:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fph9picvaddv6b4dxkjn.png) 

{% enddetails %}

Let's get data from this API in our dashboard Angular controller!

First I like to set up an init function that I call at the end of the controller, which can then be used to run functions you want to run right when the dashboard is loaded. Then you can also set a `vm.loading` variable so you can display the built in `<umb-load-indicator>`. We also want to inject the Angular $http service for calling our Api controller. Here is an initial setup:

```js
(function () {
    "use strict";

    function DashboardController($http) {

        var vm = this;

        function init() {            
            getContentForRelease();            
        }

        function getContentForRelease() {
            vm.loading = true;
            $http({
                method: 'GET',
                url: '/Umbraco/backoffice/api/Dashboard/GetContentForReleaseNextWeek',
                headers: {
                    'Content-Type': 'application/json'
                }
            }).then(function (response) {
                console.log(response); // logging the response so we know what to do next!
                
                vm.loading = false;
            });
        }

        init();

    }

    angular.module("umbraco").controller("MyDashboardController", DashboardController);

})();
```
To start with I am just console.log()'ing the response so I know the data structure for the next step:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g6ytc5smskg93hitmy7p.png)
 
So now that we know the data coming back, let's amend the `getContentForRelease` function:

```js
function getContentForRelease() {
    vm.loading = true;
    $http({
        method: 'GET',
        url: '/Umbraco/backoffice/api/Dashboard/GetContentForReleaseNextWeek',
        headers: {
            'Content-Type': 'application/json'
        }
    }).then(function (response) {
        console.log(response); // logging the response so we know what to do next!

        if (response.data.length > 0) {
            vm.weHaveRecords = true;
            vm.records = response.data;
        } else {
            vm.weHaveRecords = false;
        }               
                
        vm.loading = false;
    });
}
```

and the view:

```html
<div ng-controller="MyDashboardController as vm">
    <h1>Welcome to my dashboard!</h1>

    <umb-box ng-if="vm.weHaveRecords">
        <umb-box-content>
            <!-- The following is a super nice way to just get an overview of the data you get and make sure the data is there! -->
            {{vm.records | json}}
        </umb-box-content>
    </umb-box>

    <umb-box ng-if="!vm.weHaveRecords">
        <umb-box-content>
            No records at this point!
        </umb-box-content>
    </umb-box>

    <umb-load-indicator ng-if="vm.loading">
    </umb-load-indicator>
</div>
```

And it outputs the data!

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bc40q788frqc4mz43rm0.png)

# Step 3 - Getting more relevant data

So let's look at the data we have available and decide what we want to display in the listview:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3tnlsz1568f0fghpdenk.png)

So when looking at this data, here is what I think would make sense to include in the listview:

- Content name 

Not a lot more useful in the data - here is the thing though, I would probably want some other data as well:

- Url
- Scheduled publish date
- Who scheduled it for publishing
- Maybe a breadcrumb?

## Getting the url

Now the Url you can kind of generate, since the data has both an `Id` and `Key` - if you don't know already you can visit any content node on a fixed path with id or guid, so with the data above I know I can go to both:
`https://localhost:44303/umbraco#/content/content/edit/202cdc2f-1a45-40f4-a653-a7b321e1c54c`
`https://localhost:44303/umbraco#/content/content/edit/1110`

So I can use either one as the URL. The other info is not immediately available though, so let's see what we can do!

## Getting the scheduled publish date

This proved to be a bit difficult, but I found a way to [get it from IContent](https://our.umbraco.com/apidocs/v8/csharp/api/Umbraco.Core.Models.IContent.html#Umbraco_Core_Models_IContent_ContentSchedule). So I decided to change the API controller a little bit:

_DashboardController.cs_
```csharp
public class DashboardController : UmbracoAuthorizedApiController
{
    private readonly IContentService _contentService;

    public DashboardController(IContentService contentService)
    {
        _contentService = contentService;
    }

    public List<ContentWithScheduledInfo> GetContentForReleaseNextWeek ()
    {
        var endDate = DateTime.Now.AddDays(7);

        var response = _contentService.GetContentForRelease(endDate);

        var contentWithScheduledInfo = new List<ContentWithScheduledInfo>();

        foreach(IContent item in response)
        {
            contentWithScheduledInfo.Add(new ContentWithScheduledInfo
            {
                Content = item,
                ScheduleInfo = item.ContentSchedule
            });
        }

        return contentWithScheduledInfo; 
    }
}

public class ContentWithScheduledInfo
{
    public IContent Content { get; set; }
    public ContentScheduleCollection ScheduleInfo { get; set; }
}
```

The new content output looks like this:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o4auxdgamvchbvdfeijh.png)

So in `response.data[0].ScheduleInfo.FullSchedule[0].Date` we have the date for the scheduling, it also contains an `Action` which is either 0 or 1 based on [this enum](https://github.com/umbraco/Umbraco-CMS/blob/release-8.12.0/src/Umbraco.Core/Models/ContentScheduleAction.cs#L6) so if it's 0 it's a scheduled publish and a 1 is a scheduled unpublish.

# Outro

This blogpost is getting quite long at this point, so I've decided to split it up in atleast 2 parts 😄

In the next part we will look at how we can get info on who scheduled the publishing, present all the data in a listview and try to open the content nodes in an infinite editor window when you click a node!

If you like this post, have any feedback, suggestions, improvements to my hacky code or anything else please let me know on Twitter - [@jespermayn](https://twitter.com/JesperMayn)