-
Notifications
You must be signed in to change notification settings - Fork 26
Handling Images and Files with property Transformers
images and files can be added to your ViewModels by using IFormFile
. property transformers are attributes which can be used to modify how data is stored and indexed. it's best to explain with an example so let's look at how images are handled in Puck by default.
let's look at a simple ViewModel with an image:
public class Page:BaseModel
{
[UIHint("PuckImage")]
public PuckImage Image { get; set; }
}
the Page
ViewModel above has a property of type PuckImage
, which is a model which comes with Puck. let's look at PuckImage
to see its properties.
[PuckImageTransformer()]
public class PuckImage
{
public string Path { get; set; }
public string Description { get; set; }
public long Size {get;set;}
public string Extension { get; set; }
public int? Width { get; set; }
public int? Height { get; set; }
public List<CropModel> Crops { get; set; }
public IFormFile File { get; set; }
}
as you can see above, PuckImage
is a pretty straightforward class with mostly properties for image metadata such as Width
and Height
. Notice there is also a IFormFile
property for file upload and also notice that there is a Transformer attribute above the class, [PuckImageTransformer()]
. this attribute will process the image when the ViewModel is saved and extract the metadata from the uploaded file.
let's take a look at the source code for PuckImageTransformer
:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
public class PuckImageTransformer : Attribute, I_Property_Transformer<PuckImage, PuckImage>
{
public async Task<PuckImage> Transform(BaseModel m,string propertyName,string ukey,PuckImage p,Dictionary<string,object> dict)
{
try
{
if (p.File == null || string.IsNullOrEmpty(p.File.FileName))
return null;
string filepath = string.Concat("~/wwwroot/Media/", m.Id, "/", m.Variant, "/", ukey, "_", p.File.FileName);
string absfilepath =ApiHelper.MapPath(filepath);
new FileInfo(absfilepath).Directory.Create();
using (var stream = new FileStream(absfilepath, FileMode.Create)) {
p.File.CopyTo(stream);
}
p.Path = filepath.Replace("~/wwwroot","");
p.Size = p.File.Length;
p.Extension=Path.GetExtension(p.File.FileName);
var img = Image.Load(absfilepath);
p.Width = img.Width;
p.Height = img.Height;
}catch(Exception ex){
}finally {
p.File = null;
}
return p;
}
}
it's mostly straightforward. there's a Transform method where all the action happens but first let's look at the interface PuckImageTransformer
implements - I_Property_Transformer<T,T>
. the first Type parameter is for the final argument of the Transform method and the second Type parameter is for the return type of the Transform method. in this instance, both Type parameters are of PuckImage
, meaning that the Transform method accepts a PuckImage
argument and returns a PuckImage
too.
now let's look at all the arguments sent to the Transform method: BaseModel m,string propertyName,string ukey,PuckImage p
. the first argument is the current ViewModel being saved cast as a BaseModel
, the string propertyName
is the name of the property being transformed and the ukey
is the unique key which is just the property name including array index if the property is inside an array - for example for a property inside an array the propertyName
might be Names where the ukey
might be Names[0]. Next is the property being transformed which in this case, is a PuckImage
and the final argument is a dictionary which helps you store information that can be referenced between transformers.
if you look at the body of the Transform method, it's just saving the image to a directory and setting the Path
to the saved file and setting other metadata.
this is good practice, although there is a default transformer that sets all IFormFile properties to null anyway. you can see the PuckImageTransformer
doing this in the finally{}
block.
there is a second image transformer you can use with PuckImage
and that's the [PuckAzureBlobImageTransformer()]
. this transformer handles images by saving them to Azure blob storage and saving a path to the file. to use this transformer, you need to set the AzureImageTransformer_AccountName
and AzureImageTransformer_AccessKey
AppSettings in the appSettings.json with your Azure storage account credentials.
rather than adding an image directly to your ViewModel, you can have a dedicated Image ViewModel. by default there's one in the ViewModels
folder of the PuckWeb
project, called ImageVM
.
this way, you can save an image as a dedicated piece of content in your site content tree, maybe in an "Images" folder and then link it to other content items using the PuckImagePicker
content selector.
you can then re-use the same image by linking it to multiple pieces of content.
let's take a quick look at the source for the Azure Blob Image Transformer to see an example:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
public class PuckAzureBlobImageTransformer : Attribute, I_Property_Transformer<PuckImage, PuckImage>
{
string accountName;
string accessKey;
string containerName;
public void Configure(IConfiguration config) {
this.accountName = config.GetValue<string>("AzureImageTransformer_AccountName");
this.accessKey = config.GetValue<string>("AzureImageTransformer_AccessKey");
this.containerName = config.GetValue<string>("AzureImageTransformer_ContainerName");
}
public async Task<PuckImage> Transform(BaseModel m,string propertyName,string ukey,PuckImage p,Dictionary<string,object> dict)
{
//code omitted for brevity
}
}
as you can see, there's a Configure
method and a Transform
method. the Configure method gets called first and is passed any dependencies in the list of arguments. you can then store these dependencies in variables which you can access when the Transform
method is called afterward. you don't need to have a Configure
method. if it's present, it will be injected.
you'll obviously need to register your dependencies in the ConfigureServices
method of your Startup
class.