Accessing ViewData in Layout for page with partial view not working - asp.net-core

From a partial view, I want to include scripts or styles and have them rendered into the header or footer (instead of inline) so I have a taghelper and htmlextension that works when I used TempData in the htmlextensions, but if I use ViewData, it doesn't work. Any ideas why?
Partial view:
<style asp-resource-location="Header">
.partial1 {
background-color: red;
}
</style>
<h2>Test Partial</h2>
<script asp-resource-location="Footer">
alert("Partial1");
</script>
Htmlextensions:
public static IHtmlContent InlineScripts(this IHtmlHelper html, Enums.ResourceLocation location)
{
var result = new StringBuilder();
var scripts = html.ViewData.ContainsKey(location.ToString()) ? html.ViewData[location.ToString()] as List<string> : new List<string>();
foreach (var script in scripts)
{
result.Append(script);
}
var tag = new TagBuilder(location == Enums.ResourceLocation.Header ? "style" : "script");
tag.InnerHtml.SetHtmlContent(result.ToString());
return tag;
}
public static void AddInlineScriptParts(this IHtmlHelper html, Enums.ResourceLocation location, string script)
{
var scripts = html.ViewData.ContainsKey(location.ToString()) ? html.ViewData[location.ToString()] as List<string> : new List<string>();
scripts.Add(script);
html.ViewData[location.ToString()] = scripts;
}
Layout page:
#Html.InlineScripts(Enums.ResourceLocation.Header)
Style Taghelper:
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (Location != Enums.ResourceLocation.Header)
return;
var viewContextAware = _htmlHelper as IViewContextAware;
viewContextAware?.Contextualize(ViewContext);
var style = output.GetChildContentAsync().Result.GetContent();
_htmlHelper.AddInlineScriptParts(Location, style);
output.SuppressOutput();
}

I used ViewContext.HttpContext.Items and it works. I figured it would be better to use than TempData which uses session. If anyone has any reason why I shouldn't use this, please let me know. And if anyone knows why ViewData doesn't work, I would be interested to know also.

Related

Sitefinity search index scope - add ihttphandler task

Is there is a way I can add an ihttphandler to the search index scope. I have a ihttphandler processrequest that collecting external data and then doing an updateIndex. But I wanted to see if I can trigger that when the re-index task is completed.
public class ExternalIndexerHandler : IHttpHandler
So I think you want to run your custom code on the ToPublishingPoint method, when it's done right?
Global.asax
PublishingSystemFactory.UnregisterPipe(PageInboundPipe.PipeName);
PublishingSystemFactory.RegisterPipe(PageInboundPipeCustom.PipeName, typeof(PageInboundPipeCustom));
Now the pipe...
public class PageInboundPipeCustom : PageInboundPipe
{
public override void ToPublishingPoint()
{
//Index has completed, time to do whatever now...
base.ToPublishingPoint();
var itemsToAdd = new List<WrapperObject>();
//Make sure its only running for a specific index
if (this.PipeSettings.PublishingPoint.Name == "YourIndexName")
{
var externalStuff = this.GetExternalStuff();
foreach (var s in externalStuff )
{
var item = new AppendixIndexItem(s);
var itemToAdd = new WrapperObject(item);
Debug.WriteLine(item.IdentityField);
//Metadata... if needed
//itemToAdd.SetOrAddProperty("Tags", "Educational Material");
itemsToAdd.Add(itemToAdd);
}
if (itemsToAdd.Count > 0)
{
this.PublishingPoint.AddItems(itemsToAdd);
}
}
}
public List<string> GetExternalStuff() {
var items = new List<string>();
//Callback to your external stuff?
return items;
}
}
Is that what you're looking for?
Steve McNiven-Scott

How in razor core to create a link that has all current parameters plus some more

I'm on a page SomePage?A=a&B=b&...
I want to construct a URL that has all of the current GET parameters plus some more from an IDictionary<string, string> that I have.
The tag helper asp-all-route-data="#myDictionary" will get set the parameters from my dictionary, but I don't understand:
how to create a link with all of the current parameters; or
how to add extra parameters to such a link.
Well this works, but I think it's a bit crap because:
this feels like a really obvious thing to want to so so I don't believe that there isn't an out of the box way to do it,
I can't get the extension method to work -- it has to be called as MakeGet(this, d) rather than just MakeGet(d), and
Shouldn't we be using something like a NameValueCollection that models multiple keys as are supported in GET?
public static IDictionary<string, string> MakeGet<T>(this RazorPage p, IDictionary<string, T> d)
{
return MakeGet(p, d.ToDictionary(z => z.Key, z => { try { return z.ToString(); } catch { return null; } }));
}
public static IDictionary<string, string> MakeGet(this RazorPage p, IDictionary<string, string> d)
{
Dictionary<string, string> result = new Dictionary<string, string>();
foreach (string k in d.Keys)
{
if (!string.IsNullOrWhiteSpace(d[k]))
{
result.Add(k, d[k]);
}
}
IQueryCollection get = p.ViewContext.HttpContext.Request.Query;
foreach (KeyValuePair<string, StringValues> q in p.ViewContext.HttpContext.Request.Query)
{
if (!result.Keys.Contains(q.Key))
{
result.Add(q.Key, string.Join(",", q.Value));
}
}
return result;
}
The next problem is how to subsequently remove a parameter in the controller in order to do a redirect.
public async Task<IActionResult> OnGetTableDeleteAsync()
{
// Need to remove parameter LineNumber.
return RedirectToAction("Get");
}
Well this is what I've come up with for the parameter removal.
public static RouteValueDictionary QueryWithout(this PageModel p, params string[] remove)
{
RouteValueDictionary q = new RouteValueDictionary();
foreach (var kv in (QueryHelpers.ParseQuery(p.Request.QueryString.Value).Where(z => !remove.Contains(z.Key))))
{
q.Add(kv.Key, kv.Value);
}
return q;
}
Being used like this
public async Task<IActionResult> OnGetTableDeleteAsync(int lineNumber)
{
ImportStagingRecord i = _context.ImportStagingRecords.Find(FileId, lineNumber);
if( i != null)
{
_context.ImportStagingRecords.Remove(i);
await _context.SaveChangesAsync();
}
Microsoft.AspNetCore.Routing.RouteValueDictionary q = BaseUri.QueryWithout(this, "LineNumber", "handler");
return RedirectToAction("", q);
}
Again, I think it's crap.
(The query string contains lots of parameters for sorting, filtering, and paging the table of ImportStagingRecords which need to be preserved across requests.)

How can I change css directly(without variable) in Blazor?

I am using the server-side of Blazor.
I want to change the CSS of the body.
In Jquery I can write the code like this easily:
$("body").css("overflow-y","hidden");
However, with this tutorial(Blazor Change Validation default css class names) said, it seems I can only change the CSS by changing the class name.
It is so complex while crossing the component, especially the body is at the top of all the components.
I wonder whether there is a way can changes CSS directly in Blazor. Thank you.
There are several ways of getting out of the "blazor way" of doing things and accomplishing css modification of an element.
Simplest: Just like you can use the class attribute, use the style attribute
<element style=#myStyle></element>
#code {
string myStyle;
void MyMethod() {
myStyle="overflow-y: hidden;"
}
}
Advanced: Use JS interop
a. In the main view (index.html or Pages/_Host.cshtml depending on project type), create a js endpoint for your component
<script>
window.applyStyleForElement = function(styleOp) {
document.getElementById(styleOp.id).style[styleOp.attrib] = styleOp.value;
}
</script>
b. In razor file:
#Inject IJRRuntime JSRuntime
<element id=#myId></element>
#code {
string myId = Guid.NewGuid().ToString("n");
async Task MyMethod() {
await JSRuntime.InvokeAsync("applyStyleForElement",
new { id = myId, attrib = "overflowY", value = "hidden" });
}
}
Finally, applying to your special case with body element ("advanced" method above).
a. In the main view (index.html or Pages/_Host.cshtml depending on project type), create a js endpoint
<script>
window.applyStyleForBody = function(style) {
document.body.style[style.attrib] = style.value;
}
</script>
b. In razor file:
#Inject IJRRuntime JSRuntime
(...)
#code {
async Task MyMethod() {
await JSRuntime.InvokeAsync("applyStyleForBody",
new { attrib = "overflowY", value = "hidden" });
}
}
Well, Blazor does not support direct css modification yet, since Web Assembly doesn't. Anyway heads up, it is on the road-map for Web Assembly/Blazor.
Therefor your best bet is, changing the class name with variables. At least for now.
I'M NOT SURE IT'S THE RECOMMENDED WAY BUT it works!
For one of my project, I include a in the page html itself, using params :
<style>
html
{
background-color:#_ColorCss;
}
</style>
//html stuff here
#code
{
public string Color{ get; set; } = "white";
string _ColorCss => $"{Color}"; //use this in case of formatting (ex : add 'px' or that kind of things)
//code stuff here
}
not the very sexiest way but it works
have fun !
Well, actually there is a way to do that and it works really good (it might suffer a little delay though).
I know this answer is a little bit late but it might help other people who face the same challenge.
We need to create some JS code that includes the wanted files:
function includeLeftStyle() {
appendStyle("left.css");
}
function includeRightStyle() {
appendStyle("right.css");
}
function appendStyle(path) {
var element = document.createElement("link");
element.setAttribute("rel", "stylesheet");
element.setAttribute("type", "text/css");
element.setAttribute("href", path);
document.getElementsByTagName("head")[0].appendChild(element);
}
The wished CSS can be called according to the language (any other coditions) in the MainLayout:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (// is left)
{
await JSRuntime.InvokeAsync<object>("includeLeftStyle");
}
else
{
await JSRuntime.InvokeAsync<object>("includeRightStyle");
}
}
}
Happy coding! :)
Beginnig from the #Tewr answer, we can also change the whole class:
a) In the main view (index.html or Pages/_Host.cshtml depending on project type), create a js endpoint
<script>
window.applyStyleForElement = function (styleOp) {
if (styleOp != null) {
document.getElementById(styleOp.id).className = styleOp.value;
}
}
</script>
b) Then in the razor file
async Task MyMethod(string sortColumn)
{
await JsRuntime.InvokeVoidAsync("applyStyleForElement",
new { id = sortColumn, value = "newClassName" });
}

How do I use IViewLocationExtender with Razor Pages to render device specific pages

Currently we are building a web application, desktop first, that needs device specific Razor Pages for specific pages. Those pages are really different from their Desktop version and it makes no sense to use responsiveness here.
We have tried to implement our own IViewLocationExpander and also tried to use the MvcDeviceDetector library (which is basically doing the same). Detection of the device type is no problem but for some reason the device specific page is not picked up and it is constantly falling back to the default Index.cshtml.
(edit: We're thinking about implementing something based on IPageConvention, IPageApplicationModelProvider or something ... ;-))
Index.mobile.cshtml
Index.cshtml
We have added the following code using the example of MvcDeviceDetector:
public static IMvcBuilder AddDeviceDetection(this IMvcBuilder builder)
{
builder.Services.AddDeviceSwitcher<UrlSwitcher>(
o => { },
d => {
d.Format = DeviceLocationExpanderFormat.Suffix;
d.MobileCode = "mobile";
d.TabletCode = "tablet";
}
);
return builder;
}
and are adding some route mapping
routes.MapDeviceSwitcher();
We expected to see Index.mobile.cshtml to be picked up when selecting a Phone Emulation in Chrome but that didnt happen.
edit Note:
we're using a combination of Razor Views/MVC (older sections) and Razor Pages (newer sections).
also not every page will have a mobile implementation. That's what would have a IViewLocationExpander solution so great.
edit 2
I think the solution would be the same as how you'd implement Culture specific Razor Pages (which is also unknown to us ;-)). Basic MVC supports Index.en-US.cshtml
Final Solution Below
If this is a Razor Pages application (as opposed to an MVC application) I don't think that the IViewLocationExpander interface is much use to you. As far as I know, it only works for partials, not routeable pages (i.e. those with an #page directive).
What you can do instead is to use Middleware to determine whether the request comes from a mobile device, and then change the file to be executed to one that ends with .mobile. Here's a very rough and ready implementation:
public class MobileDetectionMiddleware
{
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext context)
{
if(context.Request.IsFromAMobileDevice())
{
context.Request.Path = $"{context.Request.Path}.mobile";
}
await _next.Invoke(context);
}
}
It's up to you how you want to implement the IsFromAMobileDevice method to determine the nature of the user agent. There's nothing stopping you using a third party library that can do the check reliably for you. Also, you will probably only want to change the path under certain conditions - such as where there is a device specific version of the requested page.
Register this in your Configure method early:
app.UseMiddleware<MobileDetectionMiddleware>();
I've finally found the way to do it convention based. I have implemented a IViewLocationExpander in order to tackle the device handling for basic Razor Views (including Layouts) and I've implemented IPageRouteModelConvention + IActionConstraint to handle devices for Razor Pages.
Note: this solution only seems to be working on ASP.NET Core 2.2 and up though. For some reason 2.1.x and below is clearing the constraints (tested with a breakpoint in a destructor) after they've been added (can probably be fixed).
Now I can have /Index.mobile.cshtml /Index.desktop.cshtml etc. in both MVC and Razor Pages.
Note: This solution can also be used to implement a language/culture specific Razor Pages (eg. /Index.en-US.cshtml /Index.nl-NL.cshtml)
public class PageDeviceConvention : IPageRouteModelConvention
{
private readonly IDeviceResolver _deviceResolver;
public PageDeviceConvention(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void Apply(PageRouteModel model)
{
var path = model.ViewEnginePath; // contains /Index.mobile
var lastSeparator = path.LastIndexOf('/');
var lastDot = path.LastIndexOf('.', path.Length - 1, path.Length - lastSeparator);
if (lastDot != -1)
{
var name = path.Substring(lastDot + 1);
if (Enum.TryParse<DeviceType>(name, true, out var deviceType))
{
var constraint = new DeviceConstraint(deviceType, _deviceResolver);
for (var i = model.Selectors.Count - 1; i >= 0; --i)
{
var selector = model.Selectors[i];
selector.ActionConstraints.Add(constraint);
var template = selector.AttributeRouteModel.Template;
var tplLastSeparator = template.LastIndexOf('/');
var tplLastDot = template.LastIndexOf('.', template.Length - 1, template.Length - Math.Max(tplLastSeparator, 0));
template = template.Substring(0, tplLastDot); // eg Index.mobile -> Index
selector.AttributeRouteModel.Template = template;
var fileName = template.Substring(tplLastSeparator + 1);
if ("Index".Equals(fileName, StringComparison.OrdinalIgnoreCase))
{
selector.AttributeRouteModel.SuppressLinkGeneration = true;
template = selector.AttributeRouteModel.Template.Substring(0, Math.Max(tplLastSeparator, 0));
model.Selectors.Add(new SelectorModel(selector) { AttributeRouteModel = { Template = template } });
}
}
}
}
}
protected class DeviceConstraint : IActionConstraint
{
private readonly DeviceType _deviceType;
private readonly IDeviceResolver _deviceResolver;
public DeviceConstraint(DeviceType deviceType, IDeviceResolver deviceResolver)
{
_deviceType = deviceType;
_deviceResolver = deviceResolver;
}
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
return _deviceResolver.GetDeviceType() == _deviceType;
}
}
}
public class DeviceViewLocationExpander : IViewLocationExpander
{
private readonly IDeviceResolver _deviceResolver;
private const string ValueKey = "DeviceType";
public DeviceViewLocationExpander(IDeviceResolver deviceResolver)
{
_deviceResolver = deviceResolver;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
var deviceType = _deviceResolver.GetDeviceType();
if (deviceType != DeviceType.Other)
context.Values[ValueKey] = deviceType.ToString();
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var deviceType = context.Values[ValueKey];
if (!string.IsNullOrEmpty(deviceType))
{
return ExpandHierarchy();
}
return viewLocations;
IEnumerable<string> ExpandHierarchy()
{
var replacement = $"{{0}}.{deviceType}";
foreach (var location in viewLocations)
{
if (location.Contains("{0}"))
yield return location.Replace("{0}", replacement);
yield return location;
}
}
}
}
public interface IDeviceResolver
{
DeviceType GetDeviceType();
}
public class DefaultDeviceResolver : IDeviceResolver
{
public DeviceType GetDeviceType() => DeviceType.Mobile;
}
public enum DeviceType
{
Other,
Mobile,
Tablet,
Normal
}
Startup
services.AddMvc(o => { })
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddRazorOptions(o =>
{
o.ViewLocationExpanders.Add(new DeviceViewLocationExpander(new DefaultDeviceResolver()));
})
.AddRazorPagesOptions(o =>
{
o.Conventions.Add(new PageDeviceConvention(new DefaultDeviceResolver()));
});

Rendering #Html.Action("actionName","controllerName") at runtime , fetching from database in MVC4

My requirement is to fetch html data from database and render it on view. But if that string contains #Html.Action("actionName","controllerName"), i need to call perticular controller action method also.
I am rendering my html on view using #Html.Raw().
Eg: Below is the html string stored in my database
'<h2> Welcome To Page </h2> <br/> #Html.Action("actionName", "controllerName")'
So when it render the string, it execute mentioned controller and action too.
Any help will be appreciated.
You can try RazorEngine to allow string template in razor executed.
For example, sample code from the project site http://antaris.github.io/RazorEngine/:
using RazorEngine;
using RazorEngine.Templating; // For extension methods.
string template = "Hello #Model.Name, welcome to RazorEngine!";
var result =
Engine.Razor.RunCompile(template, "templateKey", null, new { Name = "World" });
But there is one catch, Html and Url helpers are defined in the Mvc framework, hence it is not supported by default.
I will suggest you try to create your template by passing model so that you don't have to use #Html.Action.
If you can not avoid it, then there is possible a solution suggested by another so answer https://stackoverflow.com/a/19434112/2564920:
[RequireNamespaces("System.Web.Mvc.Html")]
public class HtmlTemplateBase<T>:TemplateBase<T>, IViewDataContainer
{
private HtmlHelper<T> helper = null;
private ViewDataDictionary viewdata = null;
public HtmlHelper<T> Html
{
get
{
if (helper == null)
{
var writer = this.CurrentWriter; //TemplateBase.CurrentWriter
var context = new ViewContext() { RequestContext = HttpContext.Current.Request.RequestContext, Writer = writer, ViewData = this.ViewData };
helper = new HtmlHelper<T>(vcontext, this);
}
return helper;
}
}
public ViewDataDictionary ViewData
{
get
{
if (viewdata == null)
{
viewdata = new ViewDataDictionary();
viewdata.TemplateInfo = new TemplateInfo() { HtmlFieldPrefix = string.Empty };
if (this.Model != null)
{
viewdata.Model = Model;
}
}
return viewdata;
}
set
{
viewdata = value;
}
}
public override void WriteTo(TextWriter writer, object value)
{
if (writer == null)
throw new ArgumentNullException("writer");
if (value == null) return;
//try to cast to RazorEngine IEncodedString
var encodedString = value as IEncodedString;
if (encodedString != null)
{
writer.Write(encodedString);
}
else
{
//try to cast to IHtmlString (Could be returned by Mvc Html helper methods)
var htmlString = value as IHtmlString;
if (htmlString != null) writer.Write(htmlString.ToHtmlString());
else
{
//default implementation is to convert to RazorEngine encoded string
encodedString = TemplateService.EncodedStringFactory.CreateEncodedString(value);
writer.Write(encodedString);
}
}
}
}
Then you have to use HtmlTemplateBase (modified base on https://antaris.github.io/RazorEngine/TemplateBasics.html#Extending-the-template-Syntax):
var config = new TemplateServiceConfiguration();
// You can use the #inherits directive instead (this is the fallback if no #inherits is found).
config.BaseTemplateType = typeof(HtmlTemplateBase<>);
using (var service = RazorEngineService.Create(config))
{
string template = "<h2> Welcome To Page </h2> <br/> #Html.Action(\"actionName\", \"controllerName\")";
string result = service.RunCompile(template, "htmlRawTemplate", null, null);
}
in essence, it is telling the RazorEngine to use a base template where mvc is involved, so that Html and Url helper can be used.