Dynamic Content Types
In general it's recommended to bake content types in to your codebase because it makes it much simpler for incoming developers to see what is available and use things like the events system with autocomplete to quickly link up what they need. However there is also some occasions where it would be useful to create a basic one via the admin panel at runtime. These are dynamic content types, and they are available via the Api/CustomContentTypes module:
socialstack i Api/CustomContentTypes
How it works
Content types created via this module are first class citizens: they have the exact same performance characteristics as ones built in to the codebase. This is because the module generates a controller, service and a C# class to represent your content type at runtime. It then runs it through the regular service startup flow which allows things like the permission system and database engine to mount the content type automatically and in the same way it would if it was built in.
Hybrid types
If you create a dynamic content type which uses the same name as any of your native types - common ones being Page or User for example - then you will actually create a hybrid type. These have all the developer advantages of being a native type whilst still being extensible from the admin panel. Naturally you just can't edit or remove the fields which originate from the underlying C# type without editing the source.
Working with one from code
If you later need to add events to a dynamic content type, you have 2 options: either generate the source of the module so it becomes built in to the code, or alternatively, you can use Services.Get to find the service:
// Get a weak reference to a service for a custom content type called "GalleryItem":
var galleryItemService = Services.Get("GalleryItemService");
// Get the event group, which is what would be the equiv of Events.GalleryItem if it existed:
var eventGroup = galleryItemService.GetEventGroup();
These weak references are not particularly ideal in many scenario's though, particularly if you did want to add an event handler to it. This is because event handlers like to know which types you're working with so you can get good autocomplete and code correctness. At the least, consider a hybrid type instead. Here though we are working with "some runtime generated content type" with unknown fields, but it is not completely unknowable. We can use generic methods to be able to add events in a generic way. Note that this works for every content type, including the built in ones, and is how e.g. the permission system mounts every type in a generic way.
// Get the service:
var service = Services.Get("GalleryItemService");
// MethodToRun is a generic method on this class which will basically accept any AutoService.
var methodToRun = GetType().GetMethod(nameof(MethodToRun));
// Create a specific flavour of that method:
var genericMethodToRun = methodToRun.MakeGenericMethod(new Type[] {
service.ServicedType,
service.IdType
});
// And invoke it:
genericMethodToRun.Invoke(this, new object[] {
service
});
// .. some time later ..
// The method itself:
public void MethodToRun<T, ID>(AutoService<T, ID> service)
where T : Content<ID>, new()
where ID : struct, IConvertible, IEquatable<ID>, IComparable<ID>
{
/*
service.EventGroup.AfterCreate.AddEventListener((Context context, T theThing) => {
// Adding an AfterCreate event handler to a totally generic content type
// because of the where T : .. rule above, C# knows it is at least some sort of content type with Id etc on it.
return new ValueTask<T>(theThing);
});
*/
}
You can be more specific with your where T: .. rule if you know the type you're working with is something more specialised such as UserCreatedContent<ID>. This gives you extra fields such as the UserId and created/ edited times.
Here's an example of triggering an acknowledgement email when a dynamic form is populated
using System.Threading.Tasks;
using Api.Contexts;
using Api.Startup;
using Api.Emails;
using Api.Database;
using System;
using Api.Eventing;
using System.Collections.Generic;
namespace Api.EventEmail
{
[LoadPriority(101)]
public partial class AutoFormEmailService : AutoService
{
private readonly EmailTemplateService _emails;
/// <summary>
/// Service to locate booking forms for events and automatically send an acknowledgement email
/// </summary>
public AutoFormEmailService(EmailTemplateService emails)
{
_emails = emails;
InstallEmails(
new EmailTemplate()
{
Name = "Event confirmation",
Subject = "Event booking confirmation",
Key = "event_confirmation",
BodyJson = "{\"c\": {\"t\": \"Email/Default\",\"d\": {},\"r\": {\"children\": {\"t\": \"Email/Centered\",\"d\": {},\"c\": [{\"t\": \"p\",\"c\": \"Thank you! Your event booking request has been receieved for\",\"d\": {},\"i\": 2},{\"t\": \"UI/Token\",\"d\": {\"mode\": \"customdata\",\"fields\": [\"event\"]},\"c\": {\"t\": \"p\",\"c\": \"${customdata.event}\",\"d\": {},\"i\": 6},\"i\": 4}],\"i\": 5},\"customLogo\": {\"t\": \"p\",\"c\": {\"t\": \"br\"},\"d\": {},\"i\": 6}},\"i\": 7},\"i\": 8}"
});
// subscribe to events triggered by content types so we can locate dynamic forms
Events.Service.AfterCreate.AddEventListener((Context ctx, AutoService service) =>
{
if (service == null)
{
return new ValueTask<AutoService>(service);
}
// Get the content type for this service and event group:
var servicedType = service.ServicedType;
if (servicedType == null)
{
// Things like the ffmpeg service.
return new ValueTask<AutoService>(service);
}
// If it's a mapping type, ignore
if (ContentTypes.IsAssignableToGenericType(servicedType, typeof(Mapping<,>)))
{
return new ValueTask<AutoService>(service);
}
if (!servicedType.Name.Contains("Form"))
{
// only interested in booking forms
return new ValueTask<AutoService>(service);
}
// setup the generic method on this class which will basically accept any AutoService.
var setupEmail = GetType().GetMethod(nameof(SetupEmail));
// Create a specific flavour of that method:
var setupEmailToRun = setupEmail.MakeGenericMethod(new Type[] {
service.ServicedType,
service.IdType
});
// And invoke it:
setupEmailToRun.Invoke(this, new object[] { service });
return new ValueTask<AutoService>(service);
});
}
/// <summary>
/// Generic method to check the 'service' fields and if ok adds the create handler
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="ID"></typeparam>
/// <param name="service"></param>
public void SetupEmail<T, ID>(AutoService<T, ID> service)
where T : Content<ID>, new()
where ID : struct, IConvertible, IEquatable<ID>, IComparable<ID>
{
// check that the content type has the required fields
JsonField emailAddressField = null;
JsonField eventField = null;
JsonField firstNameField = null;
// Get the field info for the current service type
var context = new Context();
var fieldInfo = service.GetJsonStructure(context).Result;
foreach (var fieldPair in fieldInfo.AllFields)
{
if (fieldPair.Key.ToLower() == "emailaddress")
{
emailAddressField = fieldPair.Value;
}
if (fieldPair.Key.ToLower() == "event")
{
eventField = fieldPair.Value;
}
if (fieldPair.Key.ToLower() == "firstname")
{
firstNameField = fieldPair.Value;
}
}
if (emailAddressField == null || eventField == null || firstNameField == null)
{
// form doesn't have the required fields
return;
}
// wire up the after create event for this form
service.EventGroup.AfterCreate.AddEventListener((Context context, T formData) =>
{
// Adding an AfterCreate event handler to a totally generic content type
// because of the where T : .. rule above, C# knows it is at least some sort of content type with Id etc on it.
var emailAddress = emailAddressField.FieldInfo.GetValue(formData) as string;
var eventName = eventField.FieldInfo.GetValue(formData) as string;
var firstName = firstNameField.FieldInfo.GetValue(formData) as string;
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(eventName) || string.IsNullOrWhiteSpace(firstName))
{
// form doesn't have the required data
return new ValueTask<T>(formData);
}
// now have the data so send an email.....
var userRecipient = new Recipient(emailAddress, context);
userRecipient.CustomData = new
{
Event = eventName,
FirstName = firstName
};
var recipients = new List<Recipient>
{
userRecipient
};
_emails.Send(recipients, "event_confirmation");
return new ValueTask<T>(formData);
});
}
}
}
Working with them on the UI
Dynamic content types become a lot less useful if you have to hardcode field names into your javascript in order to use them. It is recommended to make use of the Graph Editor to instead pipe the dynamic content through to more generic components.