I have a multi language site with the following structure:
siteroot
- en
-- home
-- login
-- etc.
- de
-- home
-- login
-- etc.
The content beneath the language nodes is not necessarily the same.
When switching languages I want to test if a parallel language node exists.
Currently I'm doing the following:
get current path
replace the language part of path
e.g. replace /en/login to /de/login
the closest I've found to test the existence of a page is:
XPathNodeIterator i = umbraco.library.GetXmlDocumentByUrl("http://localhost/de/login");
Debugging this shows, that umbraco actually hits the database. This can't be the best way to test the existence of a page.
Anybody have a better method at hand?
By the sounds your using the document class in cms.businesslogic.web namespace. This class is used for modifying/publishing nodes inside of umbraco.
Try using the node class that resides in umbraco.presentation.nodeFactory. This will interact with the in-memory XML cache only.
Node.GetCurrent() //static method - will give you the current loaded page.
Node.Parent //class property - will give parent method
The problem with the node class, that it can't take XPath queries (and will not give performance)
I've written a dynamic Linq provider that can be used to query the Umbraco XML structure using compiled xslt expressions. I going to be publishing in the next week or so. Let me know if your interested...
We have a multilingual site where we needed to do the same thing to set up our hreflang tags. There might be a better way, but I decided on building some Xpath to find out if a matching node exists in the other languages. We are using umbraco 7, and I would shy away from using NodeFactory if at all possible. It is depreciated. Using the umbraco helper won't hit the database, and is one of the best ways to query published content or media from umbraco for umbraco 7.
public static IPublishedContent GetLocalizedVersionOfPage(this IPublishedContent node, string regionName)
{
var umbracoHelper = new UmbracoHelper(UmbracoContext.Current);
var ancestorNames = node.AncestorsOrSelf()
.Where(n => n.Level > 1)
.OrderBy(n => n.Level)
.Select(n => n.Name).ToList();
var xpath = new StringBuilder();
xpath.AppendFormat("/root/HomePage[#nodeName='{0}']", regionName);
foreach (var ancestorName in ancestorNames)
{
xpath.AppendFormat("/*[#nodeName='{0}']", ancestorName);
}
var matchingNode = umbracoHelper.TypedContentAtXPath(xpath.ToString()).FirstOrDefault();
return matchingNode;
}
The above method is an extension method on the IPublishedContent. It allows you to pass in the region and it checks to see if a node with the same path determined by node name exists in the specified region. I thought about using the urlname instead of the node name. You could do that as well and maybe even make this faster by skipping the piece of code that does the .AncestorsOrSelf(). It just depends on how you want it to work. In my case, I wanted it to find a match based on the node name even if the url path was different, so I had to do the .AncestorsOrSelf(). Hope this helps.
Another thing to consider is how you call this method. if you use a loop like this:
#foreach (var region in Umbraco.TypedContentAtRoot().Where(n => n.IsDocumentType("HomePage")))
{
var localizedVersion = currentPage.GetLocalizedVersionOfPage(region.Name);
if (localizedVersion != null)
{
<link rel="alternate" href="#localizedVersion.UrlAbsolute()" hreflang="#LocalizeUtils.GetCulture(region.Name)" />
}
}
You will end up getting the ancestors of the current node over and over once for each region because it calls .AncestorsOrSelf() every time you call GetLocalizedVersionOfPage(). It probably makes sense to refactor the GetLocalizedVersionOfPage method so you only have to call .AncestorsOrSelf once. If you do this sort of thing too many times, it starts to affect performance (especially if your site is very nested).
Related
I'm new to Umbraco, so this may not even be feasible. I've created my own Datatype using Archetype and want to be able to get an instance of that type on the page by type, not alias.
I know that I can do the following:
model.Content.GetPropertyValue("myAlias")
But I want to know if it's feasible to get the property by the type. Something along the lines of:
model.Content.GetPropertiesByType("TypeName")
which would return a list of controls on the page of that type?
Is this feasible?
It's possible, but not exactly straight forward.
Take a look at the available Umbraco Data Services - you'll need to retrieve the DataTypeDefinitions from the DataTypeService and retrieve the ContentType for the Model's IPublishedContent using the ContentTypeService.
Once you have these, you can match up the PropertyTypes on the ContentType with the retrieved DataTypeDefionitions based on the PropertyType's DataTypeDefinitionId.
The PropertyTypes have an Alias property which will match up with the Property Aliases on the Content itself.
You can use the content service if you get the id of the datatype you trying to find the multiples of from the url when you edit/create the datatype.
#foreach (var p in ApplicationContext.Current.Services.ContentService.GetById(Model.Content.Id).PropertyTypes.Where(p => p.DataTypeDefinitionId == -89))
{
<p>#p.DataTypeDefinitionId</p>
}
I am writing an HtmlHelper extension and I need to search for the existence of a template by name. The template in question may be a display or editor template depending on the context. My initial thought was to use ViewEngines.Engines.FindPartialView method. However, it appears that this method is not searching the ~/Views/Shared/DisplayTemplates and ~/Views/Shared/EditorTemplates directories.
I suppose this is for good reason. After all, how would the ViewEngine know whether to return the display or editor template without some additional information of context?
So, that leads to the question: how can I search for a specific EditorTemplate/DisplayTemplate I've considered adding a custom view engine to the ViewEngines collection to include these locations. I'm concerned, however, that this might be problematic.
My main concern is that the DisplayTemplate/EditorTemplate view might be served up for something unintended. Does anyone else see this as a problem?
Is it a better idea just to new up a specific DisplayTemplateViewEngine/EditorTemplateViewEngine instance when necessary and keep the ViewEngines collection clear of this specific functionality?
Is there something else I'm missing?
I absolutely love that the MVC framework is open source! I was able to determine, from the TemplateHelpers class (internal to the MVC Runtime) that the DataBoundControlMode is considered when rendering a template. The answer was simple! All I have to do is prefix the template name with the appropriate template director. So, to find a display template:
var metadata = ModelMetadata.FromLambdaExpression(expression, HtmlHelper.ViewData);
ViewEngines.Engines.FindPartialView(
_controllerContext,
string.Format("DisplayTemplates/{0}", metadata.TemplateHint))
No additional view engines or routing required! In case you're interested in the application, my helper is auto-generating UI components for a given model. I wanted to enable the existence of a custom template to bypass the automated rendering.
A WebFormViewEngine has a few properties that define (patterns for) locations to search for views.
You either follow the convention of the view engine you use, or create a custom view engine (that for examlpe extends Razor) with custom view paths.
The latter is explained here:
public class CustomViewEngine : RazorViewEngine
{
public CustomViewEngine()
{
var viewLocations = new[] {
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/DisplayTemplates/{0}.cshtml",
"~/Views/Shared/DisplayTemplates/{1}/{0}.cshtml",
// etc
};
this.PartialViewLocationFormats = viewLocations;
this.ViewLocationFormats = viewLocations;
}
}
So I guess in your helper you should look up the current view engine and look up its view location paths and search them in order. Doesn't an Html helper have a method or property for getting the view you're currently running in?
Why you just map the relative path
string path = Server.MapPath("~/View/");
And then check if the file exits base on the .cshtml exit's in that specific directory
string fileName = "MyView.cshtml";
if (File.Exists(path + fileName))
//do somethings
else
//do another things
I am currently creating an MVC4 web application. This application shows products including images. I have got a company to review the SEO aspects of the site, and they have come back with a recommendation regarding product images.
Currently my image path is: folder/images/productimage/PROD_98713204_LARGE.gif
They have recommended the following: /folder/images/productimage/98713204-160x260-0-0_My+Product+Name.gif
The problem I have is that I have a large number of images on the site so it is difficult to go rename all to include product names etc. So I have thought about using the routing features within MVC, outputting the recommended in the html markup but picking the current image path shown above from the filesystem.
2 questions I have are:
Is there performance implications of using such routing to manage image paths? My site will have large traffic loads and a number of images so it is a concern.
Could someone give me an example of a route I would need to configure to achieve the above?
In order to do the routing option, you'll have to come up with some specification for how the urls map to the actual images. How many images are we talking about? 1000? 10,000? A million? If you have less than 100,000 then I'd probably go ahead and use the specification you already wrote to just go ahead and rename all the files and then use the specification to name files on the way in to the file system from now on.
The advantages of this system are that it limits the scope of the changes to the data, and you only have to affect one point of the system (when the files are on the way in). When it comes to performance, the overhead of mapping a string to another string is probably negligible, even for a large number of requests. String manipulation for short strings is very fast, but in any case you should profile the entire request if requests start taking too long and focus on the major pain points. Of course if you just rename the files, you can be sure you won't have to worry about any of this profiling.
As for creating a route to do the mapping of urls, you first have to get ASP.NET to hand the request to your code. By default ASP.NET first checks if the file exists at the location specified by the url, and if it does, it just processes the file based on the registered handler in IIS. My suggestion is to leave these settings as they are because they make very large changes to the system when you change them. For images, the handler just attempts to send the file to the client.
If the file does not exist on the disk, it then looks for a handler for the request by iterating through the route collection, which is the thing you register your routes into typically in Global.asax. Since you didn't tell us what you tried, I'm going to assume that you know how routes work. You can get pretty crafty with routes, but I'll stick to something simple:
routes.MapRoute(
name: "images",
url: "{folder}/images/productimage/{unmappedFileName}",
defaults: new { controller = "Home", action = "Images" }
);
This route will match the example url you gave. In the case that they use the actual file name however, this route will never be hit, as I have explained above. Since the SEOed file name does NOT exist however, this route will be hit and it will try to run the Images action on the Home controller (I list my entire Home controller here to remove any confusion about where these parts go):
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Images(string unmappedFileName)
{
if (String.IsNullOrWhiteSpace(unmappedFileName))
{
return HttpNotFound();
}
var fileName = MapFileName(unmappedFileName);
var diskLocation = GetDiskLocation(fileName);
return File(diskLocation, "image/png");
}
private string MapFileName(string unmappedFileName)
{
return unmappedFileName + ".png";
}
private string GetDiskLocation(string fileName)
{
var fullPath = String.Format("~/Content/themes/base/images/{0}", fileName);
var diskLocation = Server.MapPath(fullPath);
return diskLocation;
}
}
Obviously you'll need to update the file name mapping to whatever spec you decided upon. I am using what's here because there are a bunch of example files in that folder when you create a new MVC4 project.
A simple way to show that it works is to implement your Index view in the Home folder like this:
{
ViewBag.Title = "Index";
var imagesDirectory = Server.MapPath("~/Content/themes/base/images/");
var imageFileNames = Directory.GetFiles(imagesDirectory).Select(m => m.Replace(imagesDirectory, "").Replace(".png", ""));
}
<h2>Index</h2>
#foreach (var imageFileName in imageFileNames)
{
<div>#Html.ActionLink(imageFileName, "Images", new { unmappedFileName = imageFileName })</div>
}
In the future when you run into a problem like this, you should just try to figure it out first. When you ask your question, be sure to tell us what you have tried so we can get you over the next hump and point you in the right direction instead of just asking us for the code to solve your problem. In the case where you don't know where to get started, try searching for or asking a more abstract question. Who knows, they might even answer your other questions at the same time. :)
And lastly, this solution is really complicated. I don't even know how your mapping function is going to work, but I know this is complicated. It also adds a layer of complexity when debugging because now the urls you have don't directly relate to the file name on disk, and that time will add up later on. Of course there are reasons why I might favor this mapping, most notably if you intend to change the url structure in the future for further SEO changes, but then you're breaking urls on the internet and damn you for that. So really, I suggest just changing all of your file names if that is feasible.
I created a project that the nodes are defined using attributes, and I set it in the web.config to scan for attributes, and it works fine.
I don't use an XML file at all.
Now I want to add a dynamic node provider, how do I do it?
Is there a way to do it without the XML (.sitemap) file?
I need to make sure it's under the root, which has been set in code using MvcSiteMapNodeAttribute attribute.
I've read the documentation and I can't really figure out where to place this line:
<mvcSiteMapNode
title="Details" action="Details"
dynamicNodeProvider="Project.StoreDetailsDynamicNodeProvider, Prject" />
What action is it supposed to point to? Additionally as said above, the root element is defined using attributes, so my question is if there is a way to avoid XML, or alternatively what's the efficient way to declare the XML (the less the better) to include my dynamic provider.
Update
I've tried the following and the node provider still isn't reached (From HomeController.cs).
[MvcSiteMapNode(Title = "Home", Key = HomeMenuKey,
DynamicNodeProvider = "Project.Namespace.NodeProvider, Assembly")]
public ActionResult Index()
{
return View();
}
Can you define it in the controller method attributes (and not use XML at all)?
For example:
[MvcSiteMapNode(Title="Details",
DynamicNodeProvider = "Project.StoreDetailsDynamicNodeProvider, Project")]
public ActionResult Index()
{
return View();
}
Seems that the dynamicNodeProvider attribute is ignored in the root node, also when it's defined in attributes.
So the only way to add a dynamic node provider under the root, is either by specifying it on a dummy action etc. or using XML.
An interesting note: the actual difference between defining in XML and attributes is that if it's defined in attributes, it (i.e. the gen. menu items) will be last in the menu, whereas when defined in XML it will be right after the root item (I guess that would be Home), Note that this is still controllable via the Order property in the attributes.
In my Web.Config, I left the siteMapFile empty, relying in what it said in the wiki page, that the default value is ~/Web.sitemap, in fact this is false (I've already corrected that in the updated wiki).
I don't think this behavior should be like this, I do think the MvcSiteMap engine should scan for dynamic node providers just as it scans for dynamic node attributes (here is the issue I posted on site).
In my view (asp.net mvc razor) I would like to display a description (from my model) in french or dutch based on current thread culture. Below is my actual implementation.
<td>#item.Title</td>
<td>#item.SubTitle</td>
#if (Thread.CurrentThread.CurrentCulture.Name == "fr-BE") {
<td>#item.MaterialPacking.DescriptionFr</td>
} else {
<td>#item.MaterialPacking.DescriptionNl</td>
}
<td>#item.Quantity</td>
...
I think the code clarity is not optimal but I don't think creating a helper specific for this is necessary. Are there any other possibilities?
Thanks.
UPDATE
Here is an extract of data I retrieve from my repository.
As you can see I have 2 possibilities: ...fr or ...nl
I need a specific item based on the current culture.
here is the linq:
var request = requestRepository.Find(x => x.RequestID == requestID)
.MyInclude(x => x.TransportedMaterials.Select(y => y.MaterialPacking)).FirstOrDefault();
return request.TransportedMaterials;
If you use a custom ViewModel, you can make it the controller's responsibility to populate the MaterialPacking property with the message in the correct language. That moves the code out of the view.
However, the if/else statement is still bad practice. What happens if you decide to support Spanish? Do you want to modify every one of these if statements throughout the code? You should create a service where you can pass it the key for a message and it will give you back the actual message in the current language.
So your controller code would end up saying something like this:
item.MaterialPackingDescription =
_languageService.GetDescription(item.MaterialPacking);
And your view code:
<td>#item.MaterialPackingDescription</td>
You don't need special casing (it wouldn't scale: if you were to add one more language, you'd have to go in an add a new else block everywhere you do this). The way to get localized strings is to use the built-in resource manager. Visual Studio makes this very easy... Look it up (or look up localization) in MSDN.
So your code would become:
<td>#item.Title</td>
<td>#item.SubTitle</td>
<td>#Resources.MaterialPackingDescription</td>
<td>#item.Quantity</td>
...
Note that you are also using the wrong property: For resources you should use Thread.CurrentThread.CurrentUICulture rather than CurrentCulture.
Why not perform this logic in your controller or view model. Then you wouldn't need to do it in your view.
Take a look at this blogpost about Localization.
With resource files you'll be able to keep your view cleaner because there's no need for if constructions to display messages in the correct language.
You must store your string values in App_GlobalResources, and then use it from there.
Check this article for more help