For a fun side-project I'm doing in my spare time I need to expose some MNTP items to WebApi so I can consume some of the content using JavaScript.
I'm almost there - Since I'm not a backend developer and no c# guru I need some advice on how to extend the functionality so I can get the picked items exposed in my JSON too.
I need to expose some properties from my Homepage node and then some of the "related" properties from the selected elements from the MNTP.
My current model code looks like this
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Umbraco.Core.Models;
using Umbraco.Web;
namespace IsItCgYetApp.ApiModels
{
public class HomePageDateItemsModel
{
[JsonProperty("umbracoNodeId")]
public int UmbracoNodeId { get; set; }
[JsonProperty("header")]
public string Header { get; set; }
[JsonProperty("bodyText")]
public string BodyText { get; set; }
[JsonProperty("startDate")]
public DateTime StartDate { get; set; }
[JsonProperty("endDate")]
public DateTime EndDate { get; set; }
[JsonProperty("pickDatetimeElements")]
public string PickDatetimeElements { get; set; }
public static HomePageDateItemsModel GetFromContent(IPublishedContent a)
{
return new HomePageDateItemsModel
{
UmbracoNodeId = a.Id,
Header = a.GetPropertyValue<string>("header"),
BodyText = a.GetPropertyValue<string>("bodyText"),
StartDate = a.GetPropertyValue<DateTime>("startDate"),
EndDate = a.GetPropertyValue<DateTime>("endDate"),
PickDatetimeElements = a.GetPropertyValue<string>("pickDatetimeElements")
};
}
}
}
And my controller looks like this
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Skybrud.WebApi.Json;
using Skybrud.WebApi.Json.Meta;
using Umbraco.Core;
using Umbraco.Web;
using Umbraco.Web.WebApi;
using Umbraco.Core.Models;
using IsItCgYetApp.ApiModels;
namespace IsItCgYetApp.ApiControllers
{
[JsonOnlyConfiguration]
public class HomePageDateItemsController : UmbracoApiController
{
private UmbracoHelper _helper = new UmbracoHelper(UmbracoContext.Current);
public object GetAllItems(int umbracoNodeId) {
var c = _helper.TypedContent(umbracoNodeId);
return Request.CreateResponse(JsonMetaResponse.GetSuccessFromObject(c, HomePageDateItemsModel.GetFromContent));
}
}
}
And the output I'm currently getting looks like this
{
"meta": {
"code": 200
},
"data": {
"umbracoNodeId": 1061,
"header": "Nope!",
"bodyText": "<p>...Still some time to go untill <a href=\"http://codegarden15.com\" target=\"_blank\" title=\"The official Codegarden 15 website.\">#CG15</a>. Stay tuned!</p>",
"startDate": "2015-06-10T09:00:00",
"endDate": "2015-06-12T16:00:00",
"pickDatetimeElements": "1105,1106,1117,1107,1108,1109,1110,1111,1112,1113,1114,1115"
}
}
As you can see in the "pickDatetimeElements" property the value is a comma separated string of node id's. Instead of the nodeid's I would like to have the node items returned so I can consume the data from these instead.
The desired output should look something like this
{
"meta": {
"code": 200
},
"data": {
"umbracoNodeId": 1061,
"header": "Nope!",
"bodyText": "<p>...Still some time to go untill <a href=\"http://codegarden15.com\" target=\"_blank\" title=\"The official Codegarden 15 website.\">#CG15</a>. Stay tuned!</p>",
"startDate": "2015-06-10T09:00:00",
"endDate": "2015-06-12T16:00:00",
"pickDatetimeElements": {
"dateTimeElement": {
"header": "Header 1",
"bodyText": "Some body text 1"
},
"dateTimeElement": {
"header": "Header 2",
"bodyText": "Some body text 2"
}
}
}
}
Now I just wonder how I can achieve that?
I'm thinking that I should perhaps split the id's from the "PickDatetimeElements" property in the "GetFromContent" method and then loop them over and assign the values to an array/list and then assign the list to the "PickDateimeElements?
Can anyone help me out showing an example of this?
The last JSON is actually invalid since you have an object with the same key twice. Rather than "pickerDateElements" being an object, I think what you really want is an array instead. If that is the case, have a look at the code below.
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Umbraco.Core.Models;
using Umbraco.Web;
namespace IsItCgYetApp.ApiModels {
public class HomePageDateItemsModel {
[JsonProperty("umbracoNodeId")]
public int UmbracoNodeId { get; set; }
[JsonProperty("header")]
public string Header { get; set; }
[JsonProperty("bodyText")]
public string BodyText { get; set; }
[JsonProperty("startDate")]
public DateTime StartDate { get; set; }
[JsonProperty("endDate")]
public DateTime EndDate { get; set; }
[JsonProperty("pickDatetimeElements")]
public IEnumerable<PickerDateElement> PickDatetimeElements { get; set; }
public static HomePageDateItemsModel GetFromContent(IPublishedContent a) {
// Get the string value (?? makes sure we at least have an empty string rather than NULL)
string elementsValue = a.GetPropertyValue<string>("pickDatetimeElements") ?? "";
// Split the string and parse the IDs into integers
IEnumerable<int> elementIds = (
from str in elementsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
select Int32.Parse(str)
);
// Grab the selected nodes from the cache
IEnumerable<PickerDateElement> elements = (
from id in elementIds
let content = UmbracoContext.Current.ContentCache.GetById(id)
where content != null // check whether the content is NULL - eg. if the node has been deleted
select PickerDateElement.GetFromContent(content)
);
return new HomePageDateItemsModel {
UmbracoNodeId = a.Id,
Header = a.GetPropertyValue<string>("header"),
BodyText = a.GetPropertyValue<string>("bodyText"),
StartDate = a.GetPropertyValue<DateTime>("startDate"),
EndDate = a.GetPropertyValue<DateTime>("endDate"),
PickDatetimeElements = elements
};
}
}
public class PickerDateElement {
[JsonProperty("header")]
public string Header { get; set; }
[JsonProperty("bodyText")]
public string BodyText { get; set; }
public static PickerDateElement GetFromContent(IPublishedContent a) {
return new PickerDateElement {
Header = a.GetPropertyValue<string>("header"),
BodyText = a.GetPropertyValue<string>("bodyText")
};
}
}
}
That should do the trick ;)
(I have only tested that the code above compiles, not that it actually works)
But yes, LINQ can be a challenge if not a regular user. At Skybrud.dk we have isolated this into a couple of extension method, so we can simply call content.TypedCsvContent("propertyAlias") or even content.TypedCsvContent("propertyAlias", PickerDateElement.GetFromContent).
public static class PublishedContentExtensions {
/// <summary>
/// Converts a comma separated string into an array of integers.
/// </summary>
/// <param name="source">The comma separated string to be converted.</param>
public static int[] CsvToInt(this string source) {
return (
from piece in (source ?? "").Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
where Regex.IsMatch(piece, "^[0-9]+$")
select Int32.Parse(piece)
).ToArray();
}
/// <summary>
/// Returns whether a given node should be hidden in navigation (if the <em>umbracoNaviHide</em> property has
/// been checked in Umbraco).
/// </summary>
/// <param name="content">IPublishedContent</param>
public static bool Hidden(this IPublishedContent content) {
return content.GetPropertyValue<bool>("umbracoNaviHide");
}
/// <summary>
/// Converts a property with comma seperated IDs into a list of <code>IPublishedContent</code> by using the
/// content cache. Instances are converted to the type of <code>T</code> using the specified
/// <code>func</code>.
/// </summary>
/// <param name="content">IPublishedContent</param>
/// <param name="propertyAlias">The alias of the property containing the IDs.</param>
/// <param name="func">The delegate function to be used for the conversion.</param>
/// <returns>Collection of <code>T</code>.</returns>
public static IEnumerable<T> TypedCsvContent<T>(this IPublishedContent content, string propertyAlias, Func<IPublishedContent, T> func) {
if (func == null) throw new ArgumentNullException("func");
return (
from id in content.GetPropertyValue<string>(propertyAlias).CsvToInt()
let item = UmbracoContext.Current.ContentCache.GetById(id)
where item != null
select func(item)
);
}
}
Given this class with extension methods, Jan's code could instead look something like:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using Umbraco.Core.Models;
using Umbraco.Web;
namespace IsItCgYetApp.ApiModels {
public class HomePageDateItemsModel {
[JsonProperty("umbracoNodeId")]
public int UmbracoNodeId { get; set; }
[JsonProperty("header")]
public string Header { get; set; }
[JsonProperty("bodyText")]
public string BodyText { get; set; }
[JsonProperty("startDate")]
public DateTime StartDate { get; set; }
[JsonProperty("endDate")]
public DateTime EndDate { get; set; }
[JsonProperty("pickDatetimeElements")]
public IEnumerable<PickerDateElement> PickDatetimeElements { get; set; }
public static HomePageDateItemsModel GetFromContent(IPublishedContent a) {
return new HomePageDateItemsModel {
UmbracoNodeId = a.Id,
Header = a.GetPropertyValue<string>("header"),
BodyText = a.GetPropertyValue<string>("bodyText"),
StartDate = a.GetPropertyValue<DateTime>("startDate"),
EndDate = a.GetPropertyValue<DateTime>("endDate"),
PickDatetimeElements = a.TypedCsvContent("pickDatetimeElements", PickerDateElement.GetFromContent)
};
}
}
public class PickerDateElement {
[JsonProperty("header")]
public string Header { get; set; }
[JsonProperty("bodyText")]
public string BodyText { get; set; }
public static PickerDateElement GetFromContent(IPublishedContent a) {
return new PickerDateElement {
Header = a.GetPropertyValue<string>("header"),
BodyText = a.GetPropertyValue<string>("bodyText")
};
}
}
}
Hmm. Regarding my comment that your example wouldn't compile, it appears that generics are stripped in the notification emails. So node.GetPropertyValue<string>("header") becomes node.GetPropertyValue("header") in the email.
Oh! I just have one more final question...Say that I don't want to pass a node id to my controller in order to get the data but just want to call it and have the data by going to /Umbraco/Api/HomePage/GetAllItems ? Currently I'm using
/Umbraco/Api/HomePage/GetAllItems?umbracoNodeId=1061
I bet it's something really simple that will make me feel embarrased once I figure it out. But right now I can't wrap my head around it :-/
By declaring two method with different signatures (parameters), your code could look something like this:
public object GetAllItems() {
return GetAllItems(1061);
}
public object GetAllItems(int umbracoNodeId) {
var c = _helper.TypedContent(umbracoNodeId);
return Request.CreateResponse(JsonMetaResponse.GetSuccessFromObject(c, HomePageDateItemsModel.GetFromContent));
}
For method parameters in C#, you can also define a default value of simple types, so rather than declaring two method, you just declare one like:
public object GetAllItems(int umbracoNodeId = 1061) {
var c = _helper.TypedContent(umbracoNodeId);
return Request.CreateResponse(JsonMetaResponse.GetSuccessFromObject(c, HomePageDateItemsModel.GetFromContent));
}
It gives somewhat the same result, so it really comes down to your personal code style.
Yeah, I know that I can just hardcode the id in...but was thinking if I could just tell the controller to get the homepage properties without hard-coding the id...but of course I can't since it somehow needs to know exactly what node to get the data from...so brainfart by me :)
There are still a few options left - these two are probably the two that makes the most sense:
App Settings
Rather than hardcoding the ID somewhere in a DLL, you can add the ID to app settings in your Web.config. You can read from app settings like:
string value = WebConfigurationManager.AppSettings["NaNaNaNaNaNaNaNaBatJan"];
Or:
int value = Int32.Parse(WebConfigurationManager.AppSettings["NaNaNaNaNaNaNaNaBatJan"]);
You can find the WebConfigurationManager class in the System.Web.Configuration namespace.
XPath
Another way is using an XPath query to match a specific node - eg:
Have you tried to play with TypedContentAtRoot? If you know where your content lives under the root then it should be easy to use a combination of .Childern.First() and .Descendants.
Exposing MNTP items to WebApi
Hi guys
For a fun side-project I'm doing in my spare time I need to expose some MNTP items to WebApi so I can consume some of the content using JavaScript.
I'm almost there - Since I'm not a backend developer and no c# guru I need some advice on how to extend the functionality so I can get the picked items exposed in my JSON too.
I have been using the code from René Pjengaards Umbraco DK festival demo here https://github.com/rpjengaard/AngularApiTalk/tree/master/code
I need to expose some properties from my Homepage node and then some of the "related" properties from the selected elements from the MNTP.
My current model code looks like this
And my controller looks like this
And the output I'm currently getting looks like this
As you can see in the "pickDatetimeElements" property the value is a comma separated string of node id's. Instead of the nodeid's I would like to have the node items returned so I can consume the data from these instead.
The desired output should look something like this
Now I just wonder how I can achieve that?
I'm thinking that I should perhaps split the id's from the "PickDatetimeElements" property in the "GetFromContent" method and then loop them over and assign the values to an array/list and then assign the list to the "PickDateimeElements?
Can anyone help me out showing an example of this?
Thanks in advance.
Cheers, Jan
Hi Jan,
The last JSON is actually invalid since you have an object with the same key twice. Rather than "pickerDateElements" being an object, I think what you really want is an array instead. If that is the case, have a look at the code below.
That should do the trick ;)
(I have only tested that the code above compiles, not that it actually works)
I would add to your
HomePageDateItemsModel
:And add a class:
Then in
return new HomePageDateItemsModel
add:Damn, beat me to it. Yeah same approach but without the nasty SQL looking LINQ syntax.. ;-)
My example will compile ;)
But yes, LINQ can be a challenge if not a regular user. At Skybrud.dk we have isolated this into a couple of extension method, so we can simply call
content.TypedCsvContent("propertyAlias")
or evencontent.TypedCsvContent("propertyAlias", PickerDateElement.GetFromContent)
.Given this class with extension methods, Jan's code could instead look something like:
It gives some cleaner code :D
Hmm. Regarding my comment that your example wouldn't compile, it appears that generics are stripped in the notification emails. So
node.GetPropertyValue<string>("header")
becomesnode.GetPropertyValue("header")
in the email.Hi guys
Thanks for the input - I'll try it out and see if I can get it to work :)
Would I be able to create the PickerDateElement class as it's own .cs file to keep things clean?
/Jan
Yes, it is in fact considered best procedure to keep classes in their own files, so go ahead ;)
I Had a notion about that - Not only does your sample compile...it bloody works too! :)
So...I guess it's more like keg of beer that I owe you now - Thank you very much sir.
/Jan
Oh! I just have one more final question...Say that I don't want to pass a node id to my controller in order to get the data but just want to call it and have the data by going to /Umbraco/Api/HomePage/GetAllItems ? Currently I'm using /Umbraco/Api/HomePage/GetAllItems?umbracoNodeId=1061
I bet it's something really simple that will make me feel embarrased once I figure it out. But right now I can't wrap my head around it :-/
/Jan
By declaring two method with different signatures (parameters), your code could look something like this:
For method parameters in C#, you can also define a default value of simple types, so rather than declaring two method, you just declare one like:
It gives somewhat the same result, so it really comes down to your personal code style.
Hi Anders
Yeah, I know that I can just hardcode the id in...but was thinking if I could just tell the controller to get the homepage properties without hard-coding the id...but of course I can't since it somehow needs to know exactly what node to get the data from...so brainfart by me :)
/Jan
Hi Jan,
There are still a few options left - these two are probably the two that makes the most sense:
App Settings
Rather than hardcoding the ID somewhere in a DLL, you can add the ID to app settings in your
Web.config
. You can read from app settings like:Or:
You can find the
WebConfigurationManager
class in theSystem.Web.Configuration
namespace.XPath
Another way is using an XPath query to match a specific node - eg:
Have you tried to play with TypedContentAtRoot? If you know where your content lives under the root then it should be easy to use a combination of .Childern.First() and .Descendants.
Am on mobile so this code won't compile.. :p
is working on a reply...