I tried to load assembly dynamically from a xap file like below:
AssemblyPart asmPart = new AssemblyPart();
StreamResourceInfo streamInfo = Application.GetResourceStream(new StreamResourceInfo(xapFileStream, "application/binary"), new Uri(source, UriKind.Relative));
Assembly asm = asmPart.Load(streamInfo.Stream);
FrameworkElement element = asm.CreateInstance(className) as FrameworkElement;
Then I tried to load the user control MainPage. If MainPage is simple as:
If the
namespace TestApp
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
}
}
}
It's fine. But if the MainPage like:
namespace TestApp
{
public partial class MainPage : UserControl
{
private readonly Dictionary<IWorkbenchWidget, TabControlElement> tabs;
public MainPage()
{
InitializeComponent();
base.Loaded += new RoutedEventHandler(myHandler);
}
}
}
It failed.
So want to know if use Assembly.CreateInstance to create a instance of a class, any special request on the class?
The problem appears to be with:
base.Loaded += new RoutedEventHandler(myHandler);
You do not have an event handler named "myHandler" defined in MainPage (unless you omitted it for brevity?).
Related
I would like to call specific action method with a parameter and retrieve data from the controller in Umbraco v9.
public class SearchResultController : RenderController
{
private readonly UmbracoHelper UmbracoHelper;
private readonly IPublishedValueFallback PublishedValueFallback;
private ISearchRepository SearchRepository;
public SearchResultController(ILogger<ContentPageController> logger, ICompositeViewEngine compositeViewEngine, IUmbracoContextAccessor umbracoContextAccessor,
IPublishedValueFallback publishedValueFallback,
UmbracoHelper umbracoHelper, ISearchRepository searchRepo
)
: base(logger, compositeViewEngine, umbracoContextAccessor)
{
UmbracoHelper = umbracoHelper;
PublishedValueFallback = publishedValueFallback;
SearchRepository = searchRepo;
}
[HttpGet]
public override IActionResult Index()
{
var model = new SearchResult(CurrentPage, PublishedValueFallback);
return View("~/Views/SearchResult.cshtml", model);
}
[HttpPost]
public IActionResult SearchResult(string searchString)
{
var results = SearchRepository.SearchString(searchString);
var model = new SearchResult(CurrentPage, PublishedValueFallback);
return View("~/Views/SearchResult.cshtml", model);
}
}
View:
function Search() {
debugger;
var searchString = document.getElementById("searchLabel").value;
var url = "www.test.com/search?searchString=" + searchString;
location.href = url;
}
I have also tried with #Html.Actionlink method, but I really can't make it work to properly redirect and pass the parameter. If i type the link manually and correctly in browser, the value also gets passed into the controller (debugger shows everything is ok).
Thanks for all the Help!
With Umbraco 9 being on dotnet 5+, we now use ViewComponents for this - https://our.umbraco.com/Documentation/Reference/Templating/Mvc/ViewComponents
so for example, you might have a ViewComponent to render your SearchResult list something like this:
using Microsoft.AspNetCore.Mvc;
using MyProject.Core.Models.View;
using MyProject.Core.Services;
namespace MyProject.Core.ViewComponents
{
public class SearchResults: ViewComponent
{
private readonly ISearchRepository searchRepo;
private readonly UmbracoHelper umbraco;
private readonly IPublishedValueFallback publishedValueFallback;
public SearchResults(
UmbracoHelper umbraco,
IPublishedValueFallback publishedValueFallback,
ISearchRepository searchRepo)
{
this.searchRepo = searchRepo;
this.umbraco = umbraco;
this.publishedValueFallback = publishedValueFallback;
}
public IViewComponentResult Invoke(string searchString)
{
var results = SearchRepository.SearchString(searchString);
var model = new SearchResult(umbraco.AssignedContentITem, publishedValueFallback);
return View(model);
}
}
}
Then, create the Default ViewComponent cshtml class in the /Views/Shared/Components/SearchResults directory (named Default.cshtml) something like this:
#inherits UmbracoViewPage<SearchResult>
<!-- Your View code goes here -->
You can render ViewComponents in your templates a couple of different ways, but my favourite is to use the tag-helper format - to do so, register your assembly containing them in your _ViewImports.cshtml file:
#addTagHelper *, MyProject.Core
And then you can you can place them in your templates like so:
<vc:search-results search-string="hello"></vc:search-results>
Microsoft Docs Reference: View components in ASP.NET Core
I'm looking for a way to extend the AspNetCore MVC view discovery logic. I want to be able to inherit from a controller and have the new controller have access to the Actions of the base Controller. Is there a way to extend the view discovery logic so that you can tell a controller where to look for its vies, to look in the folder of the controller, look in a folder based on the name of the base controller, or even look in a folder based on the namespace of the controller?
~/Controllers/UserAccountController.cs
namespace App.Controllers.UserAccount
{
public class UserAccountController {
public virtual async Task<IActionResult> Action1()
{
return View();
}
}
}
~/Controllers/UserAccountExtController.cs
namespace App.Controllers.UserAccount
{
public class UserAccountExtController : UserAccountController {
public override async Task<IActionResult> Action1()
{
return View();
}
}
}
Is there a way that I can extend the view discovery logic so that it if it does not find the view in the view folder with the same name as the Controller name, that it will look in the folder based on an Attribute of the controller, or the folder of the inherited controller, the folder that the controller exists in, or a folder based on the namespace of the controller?
I ended up going with a IViewLocationExpander to solve the issue thanks to RandyBuchholz for the tip on casting the ActionContext to a ControllerActionContext, which allowed me to identify the BaseType of the controller. This allowed be to add the convention of checking the default location of the BaseController if a view didn't exist in the default location for the Controller.
public class MyViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
// list used for future extension
var alternateLocations = new List<string>();
if (context.ActionContext.ActionDescriptor is ControllerActionDescriptor descriptor)
{
var baseType = descriptor.ControllerTypeInfo.BaseType.Name;
if (!baseType.StartsWith("Controller"))
{
var baseLocation = baseType.Replace("Controller", string.Empty);
alternateLocations.Add("/Views/" + baseLocation + "/{0}.cshtml");
}
}
var locations = viewLocations.ToList();
locations.InsertRange(locations.IndexOf("/Views/Shared/{0}.cshtml") - 1, alternateLocations);
return locations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
}
}
Then just register the IViewLocationExpander in Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//...
services.Configure<RazorViewEngineOptions>(options =>
{
var expander = new MyViewLocationExpander();
options.ViewLocationExpanders.Add(expander);
});
//...
}
Some explanation about scenario but please be patient to end!!!
I have Implemented a pluggable MVC application which can register plugins which exist in Areas folder of main Project.
each plugin have some views and controller
I want to set layout for views in plugins(the plugins don't know anything about master layout in Main application)
So I investigated some ways to render views where I want to be rendered...
In Main Application my PluginBootstrapper will register all plugins in Areas folder is as:
public class PluginBootstrapper
{
public static readonly List<Assembly> PluginAssemblies = new List<Assembly>();
public static void Init()
{
var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");
foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll", SearchOption.AllDirectories))
PluginAssemblies.Add(Assembly.LoadFile(file));
PluginAssemblies.ForEach(BuildManager.AddReferencedAssembly);
// Add assembly handler for strongly-typed view models
AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolve;
}
private static Assembly AssemblyResolve(object sender, ResolveEventArgs resolveArgs)
{
var currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
// Check we don't already have the assembly loaded
foreach (var assembly in currentAssemblies)
{
if (assembly.FullName == resolveArgs.Name || assembly.GetName().Name == resolveArgs.Name)
{
return assembly;
}
}
return null;
}
}
To call Init() and register plugin in assembly file:
[assembly: PreApplicationStartMethod(
typeof(PluginBootstrapper), "Init")]
In the other side each plugin can be developed in separate solution so developed plugin have it's own AreaRegistration implementation for example for one of them I have:
public class SettingPluginAreaRegistration : AreaRegistration
{
public override string AreaName
{
get { return "SettingPlugin"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"SettingPlugin",
"SettingPlugin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
I know how we can set layout in View but this is proper way for when all the views are in same project and Views knows where the main layout is, but in plugin views must know where the main Layout located in main application?
For example in plugin view:
#{
ViewBag.Title = "Plugin View Title";
Layout = "the address of main layout in main application";
}
As this link mentioned the other way is _ViewStart but this is also not proper way because each plugin have it's own _ViewStart.
So is there a good pattern to do this such as implementing WebViewPage to override Layout :
public abstract class SitePage<T> : System.Web.Mvc.WebViewPage<T>
{
public override string Layout
{
get
{
return base.Layout;
}
set
{
base.Layout = value;
}
}
}
or make a interface to set Layout and force the plugins view to implement that interface and change the layout by the reflections in Init() in PluginBootstrapper or something else ?
UPDATE1:
Is it possible or a good way to load all WebPageBase types while registering plugins in Init() method and set Layout for each of them by reflection ?
UPDATE2:
The bad way
public class BaseController : Controller
{
private string _masterName;
public string MasterLayout
{
get
{
return _masterName;
}
set
{
_masterName = value;
}
}
}
for controller:
public class SettingController : BaseController
{
public ActionResult Index()
{
var myView = View();
myView.MasterName = MasterLayout;
return myView;
}
}
and in Init() in PluginBootstrapper something like this:
var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");
foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll", SearchOption.AllDirectories))
PluginAssemblies.Add(Assembly.LoadFile(file));
foreach (Assembly plugin in PluginAssemblies)
{
BuildManager.AddReferencedAssembly(plugin);
var controllers = plugin.DefinedTypes.Where(x => x.BaseType.Name == "BaseController");
foreach (Type t in controllers)
{
// t.InvokeMember("MasterLayout", BindingFlags.SetProperty);
PropertyInfo propertyInfo = t.GetProperty("MasterLayout");
if (propertyInfo != null)
propertyInfo.SetValue(t, /*Convert.ChangeType(*/"~/Views/Shared/_Wrapper.cshtml"/*, propertyInfo.PropertyType)*/, null);
}
}
or hard code MasterLayout to always return specific layout address.
I believe UPDATE2 is not proper way...
thanks in advance.
I have used ASP.NET Web API CacheOutput Library for my asp.net project for web API and it working fine, but have another controller from where I have a POST method and I would like to invalidate my cache from that controller.
[AutoInvalidateCacheOutput]
public class EmployeeApiController : ApiController
{
[CacheOutput(ClientTimeSpan = 100, ServerTimeSpan = 100)]
public IEnumerable<DropDown> GetData()
{
//Code here
}
}
public class EmployeesController : BaseController
{
[HttpPost]
public ActionResult CreateEmployee (EmployeeEntity empInfo)
{
//Code Here
}
}
I would like to invalidate Employees Cache when there is add\update in employee controller.
It is little tricky, but you can get it in this way:
1. On your WebApiConfig:
// Registering the IApiOutputCache.
var cacheConfig = config.CacheOutputConfiguration();
cacheConfig.RegisterCacheOutputProvider(() => new MemoryCacheDefault());
We will need of it to get the IApiOutputCache from GlobalConfiguration.Configuration.Properties, if we let the default properties' setup happen the property with the IApiOutputCache won't exists on MVC BaseController request.
2. Create a WebApiCacheHelper class:
using System;
using System.Linq.Expressions;
using System.Web.Http;
using WebApi.OutputCache.Core.Cache;
using WebApi.OutputCache.V2;
namespace MideaCarrier.Bss.WebApi.Controllers
{
public static class WebApiCacheHelper
{
public static void InvalidateCache<T, U>(Expression<Func<T, U>> expression)
{
var config = GlobalConfiguration.Configuration;
// Gets the cache key.
var outputConfig = config.CacheOutputConfiguration();
var cacheKey = outputConfig.MakeBaseCachekey(expression);
// Remove from cache.
var cache = (config.Properties[typeof(IApiOutputCache)] as Func<IApiOutputCache>)();
cache.RemoveStartsWith(cacheKey);
}
}
}
3. Then, call it from your EmployeesController.CreateEmployee action:
public class EmployeesController : BaseController
{
[HttpPost]
public ActionResult CreateEmployee (EmployeeEntity empInfo)
{
// your action code Here.
WebApiCacheHelper.InvalidateCache((EmployeeApiController t) => t.GetData());
}
}
In the past I've stuck common properties, such as the current user, onto ViewData/ViewBag in a global fashion by having all Controllers inherit from a common base controller.
This allowed my to use IoC on the base controller and not just reach out into global shared for such data.
I'm wondering if there is an alternate way of inserting this kind of code into the MVC pipeline?
The best way is using the ActionFilterAttribute. I'll show you how to use it in .Net Core and .Net Framework.
.Net Core 2.1 & 3.1
public class ViewBagActionFilter : ActionFilterAttribute
{
public ViewBagActionFilter(IOptions<Settings> settings){
//DI will inject what you need here
}
public override void OnResultExecuting(ResultExecutingContext context)
{
// for razor pages
if (context.Controller is PageModel)
{
var controller = context.Controller as PageModel;
controller.ViewData.Add("Avatar", $"~/avatar/empty.png");
// or
controller.ViewBag.Avatar = $"~/avatar/empty.png";
//also you have access to the httpcontext & route in controller.HttpContext & controller.RouteData
}
// for Razor Views
if (context.Controller is Controller)
{
var controller = context.Controller as Controller;
controller.ViewData.Add("Avatar", $"~/avatar/empty.png");
// or
controller.ViewBag.Avatar = $"~/avatar/empty.png";
//also you have access to the httpcontext & route in controller.HttpContext & controller.RouteData
}
base.OnResultExecuting(context);
}
}
Then you need to register this in your startup.cs.
.Net Core 3.1
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews(options => {
options.Filters.Add<Components.ViewBagActionFilter>();
});
}
.Net Core 2.1
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add<Configs.ViewBagActionFilter>();
});
}
Then you can use it in all views and pages
#ViewData["Avatar"]
#ViewBag.Avatar
.Net Framework (ASP.NET MVC .Net Framework)
public class UserProfilePictureActionFilter : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
filterContext.Controller.ViewBag.IsAuthenticated = MembershipService.IsAuthenticated;
filterContext.Controller.ViewBag.IsAdmin = MembershipService.IsAdmin;
var userProfile = MembershipService.GetCurrentUserProfile();
if (userProfile != null)
{
filterContext.Controller.ViewBag.Avatar = userProfile.Picture;
}
}
}
register your custom class in the global. asax (Application_Start)
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalFilters.Filters.Add(new UserProfilePictureActionFilter(), 0);
}
Then you can use it in all views
#ViewBag.IsAdmin
#ViewBag.IsAuthenticated
#ViewBag.Avatar
Also there is another way
Creating an extension method on HtmlHelper
[Extension()]
public string MyTest(System.Web.Mvc.HtmlHelper htmlHelper)
{
return "This is a test";
}
Then you can use it in all views
#Html.MyTest()
Since ViewBag properties are, by definition, tied to the view presentation and any light view logic that may be necessary, I'd create a base WebViewPage and set the properties on page initialization. It's very similar to the concept of a base controller for repeated logic and common functionality, but for your views:
public abstract class ApplicationViewPage<T> : WebViewPage<T>
{
protected override void InitializePage()
{
SetViewBagDefaultProperties();
base.InitializePage();
}
private void SetViewBagDefaultProperties()
{
ViewBag.GlobalProperty = "MyValue";
}
}
And then in \Views\Web.config, set the pageBaseType property:
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="MyNamespace.ApplicationViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
</namespaces>
</pages>
</system.web.webPages.razor>
Un-tried by me, but you might look at registering your views and then setting the view data during the activation process.
Because views are registered on-the-fly, the registration syntax doesn't help you with connecting to the Activated event, so you'd need to set it up in a Module:
class SetViewBagItemsModule : Module
{
protected override void AttachToComponentRegistration(
IComponentRegistration registration,
IComponentRegistry registry)
{
if (typeof(WebViewPage).IsAssignableFrom(registration.Activator.LimitType))
{
registration.Activated += (s, e) => {
((WebViewPage)e.Instance).ViewBag.Global = "global";
};
}
}
}
This might be one of those "only tool's a hammer"-type suggestions from me; there may be simpler MVC-enabled ways to get at it.
Edit: Alternate, less code approach - just attach to the Controller
public class SetViewBagItemsModule: Module
{
protected override void AttachToComponentRegistration(IComponentRegistry cr,
IComponentRegistration reg)
{
Type limitType = reg.Activator.LimitType;
if (typeof(Controller).IsAssignableFrom(limitType))
{
registration.Activated += (s, e) =>
{
dynamic viewBag = ((Controller)e.Instance).ViewBag;
viewBag.Config = e.Context.Resolve<Config>();
viewBag.Identity = e.Context.Resolve<IIdentity>();
};
}
}
}
Edit 2: Another approach that works directly from the controller registration code:
builder.RegisterControllers(asm)
.OnActivated(e => {
dynamic viewBag = ((Controller)e.Instance).ViewBag;
viewBag.Config = e.Context.Resolve<Config>();
viewBag.Identity = e.Context.Resolve<IIdentity>();
});
Brandon's post is right on the money. As a matter of fact, I would take this a step further and say that you should just add your common objects as properties of the base WebViewPage so you don't have to cast items from the ViewBag in every single View. I do my CurrentUser setup this way.
You could use a custom ActionResult:
public class GlobalView : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
context.Controller.ViewData["Global"] = "global";
}
}
Or even a ActionFilter:
public class GlobalView : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.Result = new ViewResult() {ViewData = new ViewDataDictionary()};
base.OnActionExecuting(filterContext);
}
}
Had an MVC 2 project open but both techniques still apply with minor changes.
You don't have to mess with actions or change the model, just use a base controller and cast the existing controller from the layout viewcontext.
Create a base controller with the desired common data (title/page/location etc) and action initialization...
public abstract class _BaseController:Controller {
public Int32 MyCommonValue { get; private set; }
protected override void OnActionExecuting(ActionExecutingContext filterContext) {
MyCommonValue = 12345;
base.OnActionExecuting(filterContext);
}
}
Make sure every controller uses the base controller...
public class UserController:_BaseController {...
Cast the existing base controller from the view context in your _Layout.cshml page...
#{
var myController = (_BaseController)ViewContext.Controller;
}
Now you can refer to values in your base controller from your layout page.
#myController.MyCommonValue
If you want compile time checking and intellisense for the properties in your views then the ViewBag isn't the way to go.
Consider a BaseViewModel class and have your other view models inherit from this class, eg:
Base ViewModel
public class BaseViewModel
{
public bool IsAdmin { get; set; }
public BaseViewModel(IUserService userService)
{
IsAdmin = userService.IsAdmin;
}
}
View specific ViewModel
public class WidgetViewModel : BaseViewModel
{
public string WidgetName { get; set;}
}
Now view code can access the property directly in the view
<p>Is Admin: #Model.IsAdmin</p>
I have found the following approach to be the most efficient and gives excellent control utilizing the _ViewStart.chtml file and conditional statements when necessary:
_ViewStart:
#{
Layout = "~/Views/Shared/_Layout.cshtml";
var CurrentView = ViewContext.Controller.ValueProvider.GetValue("controller").RawValue.ToString();
if (CurrentView == "ViewA" || CurrentView == "ViewB" || CurrentView == "ViewC")
{
PageData["Profile"] = db.GetUserAccessProfile();
}
}
ViewA:
#{
var UserProfile= PageData["Profile"] as List<string>;
}
Note:
PageData will work perfectly in Views; however, in the case of a
PartialView, it will need to be passed from the View to
the child Partial.
I implemented the ActionFilterAttribute solution from #Mohammad Karimi. It worked well as I had the same scenario as the OP. I needed to add data to every view. The action filter attribute was executed for every Razor page request, but it was also called for every web API controller request.
Razor Pages offers a page filter attribute to avoid unnecessary execution of the action filter when a web API controller request is made.
Razor Page filters IPageFilter and IAsyncPageFilter allow Razor Pages to run code before and after a Razor Page handler is run.
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Configuration;
namespace MyProject
{
// learn.microsoft.com/en-us/aspnet/core/razor-pages/filter?view=aspnetcore-6.0
// "The following code implements the synchronous IPageFilter"
// Enable the page filter using 'services.AddRazorPages().AddMvcOptions( ... )
// in the 'ConfigureServices()' startup method.
public class ViewDataPageFilter : IPageFilter
{
private readonly IConfiguration _config;
public ViewDataPageFilter(IConfiguration config)
{
_config = config;
}
// "Called after a handler method has been selected,
// but before model binding occurs."
public void OnPageHandlerSelected(PageHandlerSelectedContext context)
{
}
// "Called before the handler method executes,
// after model binding is complete."
public void OnPageHandlerExecuting(PageHandlerExecutingContext context)
{
PageModel page = context.HandlerInstance as PageModel;
if (page == null) { return; }
page.ViewData["cdn"] = _config["cdn:url"];
}
// "Called after the handler method executes,
// before the action result."
public void OnPageHandlerExecuted(PageHandlerExecutedContext context)
{
}
}
}
As per the sample in the filter methods for Razor Pages documentation, the page filter is enabled by:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages()
.AddMvcOptions(options =>
{
options.Filters.Add(new ViewDataPageFilter(Configuration));
});
}