Ektron CMS - new extension for OnAfterAdd not working - upload

I followed the direction here for adding a new extension so that I can trigger an event whenever a new image is uploaded to Ektron. I created this new file in the App_Code folder of my project:
using System;
using System.Collections.Generic;
using System.Text;
using Ektron.Cms;
using Ektron.Cms.Common;
using Ektron.Cms.Extensibility;
using Ektron.Cms.Extensibility.Content;
namespace Cms.Extensions.Samples
{
public class UploadExtension : LibraryStrategy
{
public override void OnAfterAdd(LibraryData taxonomyData, CmsEventArgs eventArgs)
{
string[] lines = { "Written on Ektron upload event!" };
System.IO.File.WriteAllLines(#"C:\Users\Public\TestFolder\WORKING.txt", lines);
var x = taxonomyData;
}
public override void OnAfterUpdate(LibraryData taxonomyData, CmsEventArgs eventArgs)
{
var x = taxonomyData;
}
public override void OnBeforeDelete(long id, CmsEventArgs eventArgs)
{
var x = id;
}
}
}
I just put in one test line for each method so that I could add a breakpoint to see if it's getting hit. I registered the new extension in objectfactory:
<objectFactory>
<objectStrategies>
<add name="Library">
<strategies>
<add name="EktronUploadExtension" type="Cms.Extensions.Samples.UploadExtension"/>
<add name="GoogleGeoCoder" type="Cms.Extensions.GoogleGeoCoder.LibraryStrategy, Cms.Extensions.GoogleGeoCoder"/>
</strategies>
</add>
</objectStrategies>
</objectFactory>
It looks like I set everything up correctly, but I attached to process and opened up my Ektron work area and uploaded a new image to the library, but none of my breakpoints (specifically the breakpoint in OnAfterAdd) got hit. I'm not sure how to debug or figure out what's wrong with my extension.
EDIT: I fixed the objectfactory.config file, but it's still not working. The breakpoints in UploadExtension.cs aren't working, and the test file that I put in the function isn't getting written when I add new library item in Ektron.

Your objectfactory.config file is incorrect. You have created a LibraryStrategy but placed it into the Content Strategy section of the objectfactory.config.
You should add a section called "Library" to the config file like so:
<add name="Library">
<strategies>
<add name="MyFirstExample"
type="Cms.Extensions.Samples.UploadExtension"/>
</strategies>
</add>

Related

How to dynamically add a controller in a ASP.NET Core 6 MVC application

I need to dynamically creates controllers in a ASP.NET Core 6 MVC application.
I found some way to somewhat achieve this but not quite.
I'm able to dynamically add my controller but somehow it reflects only on the second request.
So here is what I do: first I initialize my console app as follows:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace DynamicControllerServer
{
internal class Program
{
static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
ApplicationPartManager partManager = builder.Services.AddMvc().PartManager;
// Store thePartManager in my Middleware to be able to add controlelr after initialization is done
MyMiddleware._partManager = partManager;
// Register controller change event
builder.Services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
builder.Services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
// Add Middleware which is responsible to cactn the request and dynamically add the missing controller
app.UseMiddleware<MyMiddleware>();
app.RunAsync();
Console.WriteLine("Server has been started successfully ...");
Console.ReadLine();
}
}
}
Then my middleware looks like this: it basically detects that there is the "dynamic" keyword in the url. If so, it will load the assembly containing the DynamicController:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using System;
using System.Reflection;
namespace DynamicControllerServer
{
public class MyMiddleware
{
public RequestDelegate _next { get; }
private string dllName = "DynamicController1.dll";
static public ApplicationPartManager _partManager = null;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Path.HasValue)
{
var queryParams = httpContext.Request.Path.Value;
if(httpContext.Request.Path.Value.Contains("api/dynamic"))
{
// Dynamically load assembly
Assembly assembly = assembly = Assembly.LoadFrom(#"C:\Temp\" + dllName);
// Add controller to the application
AssemblyPart _part = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(_part);
// Notify change
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
}
}
await _next(httpContext); // calling next middleware
}
}
}
The ActionDescriptorChange provider looks like this:
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace DynamicControllerServer
{
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}
Dynamic controller is in separate dll and is very simple:
using Microsoft.AspNetCore.Mvc;
namespace DotNotSelfHostedOwin
{
[Route("api/[controller]")]
[ApiController]
public class DynamicController : ControllerBase
{
public string[] Get()
{
return new string[] { "dynamic1", "dynamic1", DateTime.Now.ToString() };
}
}
}
Here are the packages used in that project:
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
This works "almost" fine ... when first request is made to:
https://localhost:5001/api/dynamic
then it goes in the middleware and load the assembly, but returns a 404 error.
Then second request will actually work as expected:
Second request returns the expected result:
I must doing it wrong and probably my middleware is executed too late in the flow to reflect the dynamic controller right away.
Question is: what should be the proper way to achieve this?
Second question I have is say now the external dll holding our dynamic controller is updated.
How can I reload that controller to get the new definition?
Any help would be appreciated
Thanks in advance
Nick
Here is the answer to my own question in case it can help somebody out there.
It seems building and loading the controller from the middleware will always end up with failure on the first call.
This makes sense since we are already in the http pipeline.
I end up doing same thing from outside the middleware.
Basically my application detect a change in the controller assembly, unload the original assembly and load the new one.
You cannot use the Default context since it will not allow reloading different dll for same assembly:
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); // Produce an exception on updates
To be able to reload new dll for same assembly, I’m loading each controller in its own assembly context. To do that you need to create your own class deriving from AssemblyLoadContext and managing assembly load:
public class MyOwnContext: AssemblyLoadContext
{
// You can find lots of example in the net
}
When you want to unload the assembly, you just unload the context:
MyOwnContextObj.Unload();
Now to add or remove the controller on the fly, you need to keep reference of the PartManager and the ApplicationPart.
To add controller
ApplicationPart part = new AssemblyPart(assembly);
_PartManager.ApplicationParts.Add(part);
To remove:
_PartManager.ApplicationParts.Remove(part);
On course once done, still use following piece of code to acknowledge the change:
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
That allow updating controller on the fly with no interruption of service.
Hope this helps people out there.
I have done a similar solution (used for managing a web app plugins) with some differences that may help you:
List all the external assemblies in a config file or appsettings.json so all the dll names and/or addresses are known at startup
Instead of registering controllers when they are called, register them at program.cs/start up :
//Foreah dllName from settings file
var assembly = Assembly.LoadFrom(#"Base address" + dllNameLoadedFromSettings);
var part = new AssemblyPart(assembly);
services.AddControllersWithViews()
.ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
// Any other configuration based on the usage you want
Second: I usually keep plugin dlls in the bin folder so when using IIS as soon as a dll file in bin is changed the upper-level app is automatically reset. So your second question would be solved too.

Xamarin Forms RefreshView stopped working randomly

Can someone please tell me what I'm doing wrong? The RefreshView was just working yesterday and today I can't get it to work in any page. I created a brand new page with just a RefreshView and when I try to pull down it doesn't budge. Doesn't pull down, doesn't refresh, nothing. It was just working last night and today after no code changes it's not working. I've tried on the simulator and on my actual iPad. Before anyone suggests, there are no updates to any of my NuGet packages and I can't find any reference to this issue on Google.
XF: v5.0.0.2244
View:
<?xml version="1.0" encoding="utf-8" ?>
<views:MvxContentPage
x:Class="MyApp.UI.Pages.PricingPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewModels="clr-namespace:MyApp.Core.ViewModels;assembly=MyApp.Core"
xmlns:views="clr-namespace:MvvmCross.Forms.Views;assembly=MvvmCross.Forms"
Title="{Binding Title}"
x:DataType="viewModels:PricingViewModel"
x:TypeArguments="viewModels:PricingViewModel">
<views:MvxContentPage.Content>
<RefreshView Command="{Binding RefreshCommand}" IsRefreshing="{Binding IsRefreshing}">
<CollectionView ItemsSource="{Binding MenuItems}" />
</RefreshView>
</views:MvxContentPage.Content>
</views:MvxContentPage>
ViewModel:
using System.Threading;
using System.Threading.Tasks;
using MyApp.Core.ChurromigosApi;
using MyApp.Core.Services;
using MyApp.Core.ViewModels.Base;
using MvvmCross.Commands;
using MvvmCross.ViewModels;
namespace MyApp.Core.ViewModels
{
public class PricingViewModel : BaseViewModel
{
private readonly IMenuItemService menuItemService;
public PricingViewModel(IMenuItemService menuItemService)
{
this.menuItemService = menuItemService;
this.RefreshCommand = new MvxAsyncCommand(this.Refresh);
this.MenuItems = new MvxObservableCollection<MenuItem>();
this.Title = "Pricing";
}
public MvxObservableCollection<MenuItem> MenuItems { get; set; }
public IMvxAsyncCommand RefreshCommand { get; }
public bool IsRefreshing { get; set; }
public override Task Initialize()
{
this.IsRefreshing = true;
return Task.CompletedTask;
}
private async Task Refresh()
{
var allMenuItems = await this.menuItemService.GetMenuItems(CancellationToken.None);
this.MenuItems.Clear();
this.MenuItems.AddRange(allMenuItems);
}
}
}
I found a similar problem with RefreshView, though in my case instead of becoming unresponsive, the app just crashes.
It appears that we need to pay more attention to the binding Mode for the property used for the IsRefreshing indicator:
If we want the RefreshView to automatically trigger the command when the VM's binded property is changed to True, then don't include a Mode, or use Mode=TwoWay, but don't manually change the value or you'll be stuck in a loop (or include a if(IsRefreshing) return; in your command or you wont get any results)
If we want to manually set the VM's property in the backend to show/hide the IsRefreshing activity indicator without triggering the command, then use Mode=OneWay or else you will be stuck in a loop.
In your particular case it could be related to the fact that you're setting your IsRefreshing property to true, and then you never set it back to false? I would wrap your Refreshing() method's code with a try, setting IsRefreshing=false at the beginning and then =false in a finally block.

Writing to .config files

I am having an issue somewhat similar to this question: C#: Config file error
BACKGROUND
My situation is this: I am using NLog and I have setup a Database target. My app shows an installation page on first run from which I build a connection string for other purposes but would also like to save that same connection string to the NLog.config file. After much searching it would appear that NLog can be changed programatically, but for whatever reason cannot save the changes back to the .config file. Therefore, my plan was to simply do the following:
WHAT I HAVE TRIED
Change the connectionString attribute to simply be connectionStringName instead. Example: connectionStringName="NLogConnection"
Move the <connectionStrings> element from my Web.config to a separate file: ConnectionStrings.config.
Update web.config accordingly:
<connectionStrings configSource="ConnectionStrings.config" />
Write code as follows:
string filePath = HostingEnvironment.MapPath(Path.Combine(httpRequest.ApplicationPath, "ConnectionStrings.config"));
var fileMap = new ExeConfigurationFileMap
{
ExeConfigFilename = filePath
};
var configFile = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
configFile.ConnectionStrings.ConnectionStrings["NLogConnection"].ConnectionString = connectionString;
configFile.Save();
OUTCOME
Seemed like a nice plan. However.. the code above throws an error about not having a <configuration> tag and if I add that tag, the site throws an error about the tag being there! No win situation here... does anyone have any ideas on how to solve this one? Ideally it would be nice if there was a way to properly modify external .config files which are only a part of the main web config. I may have to revert to manually reading/writing the raw XML.. but I would prefer not to if there's a better/more elegant way.
SOLUTION
Many thanks to #Julian; I now have a working solution. I added this in my Global.asax:
private static void TryUpdateNLogConnectionString(string connectionString)
{
try
{
var target = LogManager.Configuration.FindTargetByName("database");
DatabaseTarget databaseTarget = null;
var wrapperTarget = target as WrapperTargetBase;
// Unwrap the target if necessary.
if (wrapperTarget == null)
{
databaseTarget = target as DatabaseTarget;
}
else
{
databaseTarget = wrapperTarget.WrappedTarget as DatabaseTarget;
}
databaseTarget.ConnectionString = connectionString;
}
catch { }
}
You can change settings programmatically in NLog, but you can't serialize those settings to the XML, yet.
What you can do:
Save the setting to the <appSettings> when changed
read the <appSettings> when needed.
eg
using System.Web.Configuration;
using System.Configuration;
Configuration config = WebConfigurationManager.OpenWebConfiguration("/");
config.AppSettings.Settings["NlogConnectionString"].Value = "NewValue";
config.Save(ConfigurationSaveMode.Modified);
Edit the connectionstring when needed in NLog:
DatabaseTarget databaseTarget = LogManager.Configuration.FindTargetByName(targetName) as DatabaseTarget;
databaseTarget.ConnectionString = System.Configuration.ConfigurationManager.AppSettings["NlogConnectionString"];

Visibility ValueConverter Update Logic to MvvmCross v3

I updated an older android project from mvvmcross v2 to mvvmcross v3.
Got one more problem now.
The visibility doesn't work, its doing nothing.
Old solution looked like this (worked fine):
In Setup.cs
protected override IEnumerable<Type> ValueConverterHolders
{
get { return new[] { typeof(Converters) }; }
}
Converters.cs
using Cirrious.MvvmCross.Converters.Visibility;
namespace Test.Droid
{
public class Converters
{
public readonly MvxVisibilityConverter Visibility = new MvxVisibilityConverter();
}
}
Any .axml (change visibility of LinearLayout):
<LinearLayout style="#style/LinearLayoutSmall" local:MvxBind="{'Visibility':{'Path':'TestIsVisible','Converter':'Visibility'}}">
New solution (doesn't work):
In Setup.cs
protected override List<Type> ValueConverterHolders
{
get { return new List<Type> { typeof(Converters) }; }
}
Converters.cs
using Cirrious.MvvmCross.Plugins.Visibility;
namespace Test.Droid
{
public class Converters
{
public readonly MvxVisibilityValueConverter Visibility = new MvxVisibilityValueConverter();
}
}
Any .axml
<LinearLayout style="#style/LinearLayoutSmall" local:MvxBind="Visibility TestIsVisible, Converter=Visibility">
There's probably a problem with the swissbinding syntax or I'm using false classes?
Any help appreciated!
UPDATE
I forgot these lines:
public override void LoadPlugins(IMvxPluginManager pluginManager)
{
pluginManager.EnsurePluginLoaded<PluginLoader>();
pluginManager.EnsurePluginLoaded<Cirrious.MvvmCross.Plugins.Visibility.PluginLoader>();
base.LoadPlugins(pluginManager);
}
I guess its necessary but now I'm having following error:
(from the MvxPluginManager Class)...
I checked all references and the dll/project *.Visibility.Droid.dll is referenced in my mainproject and everywhere else...
Without running and debugging a complete sample of your code I can't see what the problem is. One guess is that it could be in the plugin setup for visibility, but that is only a guess. The debug trace for your app might reveal some information on this.
Alternatively, it might be easier to simply try setting up a new project and getting visibility working in that, then comparing that code back to your existing app.
Value Converters in v3 are documented in https://github.com/MvvmCross/MvvmCross/wiki/Value-Converters.
The preferred way of referencing them is simply to let MvvmCross find them by reflection - see the section on https://github.com/MvvmCross/MvvmCross/wiki/Value-Converters#referencing-value-converters-in-touch-and-droid
A sample app, including visibility, is in: https://github.com/MvvmCross/MvvmCross-Tutorials/tree/master/ValueConversion - e.g. https://github.com/MvvmCross/MvvmCross-Tutorials/blob/master/ValueConversion/ValueConversion.UI.Droid/Resources/Layout/View_Visibility.axml

Bundling CSS files depending on domain of request?

I have a multi-tenant application and I'm trying to determine the simplest means of controlling which CSS files are bundled based on the url of any incoming request.
I'm thinking I can have some conditional logic inside RegisterBundles() that takes the Url as a string, and bundles accordingly:
public static void RegisterBundles(BundleCollection bundles, string tenant = null) {
if (tenant == "contoso"){
bundles.Add(new StyleBundle("~/contoso.css")
}
}
But I don't know how to pass the string into RegisterBundles, nor even if it's possible, or the right solution. Any help here would be awesome.
It is not possible to do it in RegisterBundles right now. Dynamically generating the bundle content per request will prevent ASP.net from caching the minified CSS (it's cached in HttpContext.Cache).
What you can do is create one bundle per tenant in RegisterBundles then select the appropriate bundle in the view.
Example code in the view:
#Styles.Render("~/Content/" + ViewBag.TenantName)
Edit:
As you said, setting the TenantName in a ViewBag is problematic since you have to do it per view. One way to solve this is to create a static function like Styles.Render() that selects the correct bundle name based from the current tenant.
public static class TenantStyles
{
public static IHtmlString Render(params string[] paths)
{
var tenantName = "test"; //get tenant name from where its currently stored
var tenantExtension = "-" + tenantName;
return Styles.Render(paths.Select(i => i + tenantExtension).ToArray());
}
}
Usage
#TenantStyles.Render("~/Content/css")
The bundle names will need to be in the this format {bundle}-{tenant} like ~/Content/css-test. But you can change the format ofcourse.
I think you are after a solution that allows you to dynamically control the BundleCollection. As far as I know this is currently not possible.
The bundles are configured during app start/configured per the application domain.
A future version of ASP.NET may support this feature i,e using VirtualPathProvider.
Here is some discussion.
See also this SO question.
i'm not good in english, but if you mean you need to handle which CSS file load when you run any URL in your page, i can handle css file in a controler.
First, create a controller name : ResourceController
// CREATE PATH TO CSS FOLDER, I store in webconfig <add key="PathToStyles" value="/Content/MyTheme/" />
private static string _pathToStyles = ConfigurationManager.AppSettings["PathToStyles"];
public void Script(string resourceName)
{
if (!String.IsNullOrEmpty(resourceName))
{
var pathToResource = Server.MapPath(Path.Combine(_pathToScripts, resourceName));
TransmitFileWithHttpCachePolicy(pathToResource, ContentType.JavaScript.GetEnumDescription());
}
}
public void Style(string resourceName)
{
if (!String.IsNullOrEmpty(resourceName))
{
var pathToResource = Server.MapPath(Path.Combine(_pathToStyles, resourceName));
TransmitFileWithHttpCachePolicy(pathToResource, ContentType.Css.GetEnumDescription());
}
}
private void TransmitFileWithHttpCachePolicy(string pathToResource, string contentType)
{
//DO WHAT YOU WANT HERE;
Response.ContentType = contentType;
Response.TransmitFile(pathToResource);
}
//You can handle css or js file...
private enum ContentType
{
[EnumDescription("text/css")]
Css,
[EnumDescription("text/javascript")]
JavaScript
}
In file Global.asax.cs, make sure in application start medthod, in contain the route config
protected void Application_Start()
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
Go to routeConfig, add below map to this file (must be add in top of this file) :
routes.MapRoute(
name: "Resource",
url: "resource/{action}/{resourceName}",
defaults: new { controller = "Resource" }
);
Now, create a UrlHelperExtensions class, same path with webconfig file
public static class UrlHelperExtensions
{
public static string Style(this UrlHelper urlHelper, string resourceName)
{
return urlHelper.Content(String.Format("~/resource/style/{0}", resourceName));
}
}
And from now, you can define css file in your view like :
..."<"link href="#Url.Style("yourcss.css")" rel="stylesheet" type="text/css"
/>
Hope this help

Resources