How to create a virtual node in Umbraco
Asked Answered
S

3

6

Situation

I have created a new section in Umbraco 7.3 where I can manage a catalogue (create, edit, and delete products). When I create a product, I store all its information in a table in the Umbraco database. Please note that I do not create a node in the tree, I just work directly with the database.

Question

For example, when I create the "Product A" in that catalogue, I automatically generate and store a custom URL "/products/product-A" in a table in the database. However, that URL does not exist as I do not have a physical node:

Page not found

No umbraco document matches the url '/products/product-A'.

As I do not have a physical node for it, how can I "create" a virtual node and assign a template for that URL?

Stray answered 26/2, 2016 at 8:49 Comment(0)
S
17

I could figure this out at the end. Thank you for your answers!

1. Create a class for our virtual page

First thing is to create a class i.e ProductVirtualPage which inherits from PublishedContentWrapped. This last class provides an abstract base class for IPubslihedContent implementations that wrap and extender another IPublishedContent. We will use the property Product to get the Product from the Razor view and render.

public class ProductVirtualPage : PublishedContentWrapped
{   
  private readonly Product _product;

  public ProductVirtualPage(IPublishedContent content, Product product ) : base(content)
  {
     if (product.Name == null) throw new ArgumentNullException("productName");
     _product = product;
  }

   public Product Product
   {
       get
       {
           return _product;
       }
   }
 }

2. Create our handler based on UmbracoVirtualNodeRouteHandler

In the tree we need to create a reference node, which in my case it has the id 3286. We will pass it to the ProductVirtualPage(nodeReference, product) method when the FindContent() method is called.

public class ProductVirtualNodeRouteHandler : UmbracoVirtualNodeRouteHandler
{
    private ProductService _productService;

    protected override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
    {
        // Get the real Umbraco IPublishedContent instance as a reference point
        var nodeReference = new UmbracoHelper(umbracoContext).TypedContent(3286);

        //Get the product Id from the URL (http://example.com/Products/product/57)
        var productId = umbracoContext.HttpContext.Request.Url.Segments[3];

        // Create an instance for the ProductService
        _productService = new ProductService(new ProductRepository(), new SiteRepository());

        // Get the product from the database
        var product = _productService.GetById(Convert.ToInt32(productId));

        // Get the virtual product
        return  new ProductVirtualPage(nodeReference, product);
    }
}

3. Register our custom route

We need to add our custom route using MapUmbracoRoute and bind it to the ApplicationStarted method that provides ApplicationEventHandler.

public class ContentServiceEventsController : ApplicationEventHandler
{

    protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication,
        ApplicationContext applicationContext)
    {
        // Add a route for the Products
        RouteTable.Routes.MapUmbracoRoute(
            "ProductPage",
            "Products/Product/{id}/",
            new
            {
                controller = "Product",
                action = "GetProduct"
            },
            new ProductVirtualNodeRouteHandler());
    }
}

4. Create the controller

The controller will just return the View, passing our RenderModel model.

public class ProductController : RenderMvcController
{
    public ActionResult GetProduct(RenderModel model, int id)
    {
        return View("Product", model);
    }
}

5. The view

@{
  Layout = "~/Views/Shared/_MasterLayout.cshtml";
  var productModel = Model.Content.Product;
  var product = productModel as Product; 
}
<pre>
  Product Info
  ---------------

  Id: @product.Id
  Name: @product.Name
</pre>
Stray answered 29/2, 2016 at 11:18 Comment(2)
Thanks for sharing @Filipe do you know if you can hijack the routes in umbraco so that your products appear as children of umbraco nodes?Jamille
Do you get access to Parent and Path properties? can you set these in your ProductVirtualPage class somewhere?Jamille
I
2

It sounds like you don't need your product urls to be handled by Umbraco.

You need to add a custom route into your RouteConfig.cs file e.g.

routes.MapRoute(
  name: "Products",
  url: "Products/{id}",
  defaults: new { controller = "Products", action = "Product", id = UrlParameter.Optional }
);

and then create a ProductsController

public class ProductsController : Controller
{
    public ActionResult Product(int id)
    {
        // Retrieve product from database
        // Return view
    }

If you need access to the UmbracoContext, there is a good example here: http://shazwazza.com/post/Custom-MVC-routing-in-Umbraco

Inspection answered 26/2, 2016 at 11:59 Comment(1)
First of all, thank you very much for your time and for guiding me in the right direction. The thing now is that my Product View - which requires a Product model to be passed into - is inheriting from a Shared Layout View - which needs to receive a RenderModel model, and this is causing a conflict. Please have a look at the following gist where you can see the explanation more in detail in the comment: gist.github.com/felipecruz91/…Stray
H
0

From your Gist, do you need to create the VirtualProductNode? Could you return the Umbraco page (nodeReference) and then use that page (template/controller) to work with the product data?

protected override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
{
    // Get the real Umbraco IPublishedContent instance as a reference point
    var nodeReference = new UmbracoHelper(umbracoContext).TypedContent(3286);  

     contentRequest.PublishedContent = nodeReference;

     return contentRequest.PublishedContent != null;
}

We do something similar to list pages of pdf documents that are stored in the Media section. The pdf documents are organised in folders that map to the virtual url structure. There are no pages in the Content section.

(We didn't want the editors to upload all the pdf documents and then have to go into the Content section, create a page and select the pdfs again - unnecessary extra work)

There is one page that defines the section and its template will display the correct pdfs based on the url. The template uses the url to find the corresponding folder in the Media section and list out the pdf files.

public class ProductsContentFinder : IContentFinder
{
    public bool TryFindContent(PublishedContentRequest contentRequest)
    {
        if (contentRequest != null)
        {
            // url: /store/products/product-name
            var path = contentRequest.Uri.GetAbsolutePathDecoded();
            var parts = path.Split(new[] { '/' }, System.StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length > 1)
            {
                var level1 = string.Concat('/', string.Join("/", parts.Take(2)), '/');
                var node = contentRequest.RoutingContext.UmbracoContext.ContentCache.GetByRoute(level1);
                 //find product page
                 var products = node.AncestorOrSelf("Products");

                 if (products != null)
                 {
                     contentRequest.PublishedContent = products;
                 }
             }
         }

         return contentRequest.PublishedContent != null;
     }
 }
Hector answered 28/2, 2016 at 5:10 Comment(1)
I could not implemente the FindContent method as it appears in your code. The return type based on the method signature is IPublishedContent, however in your code you return a bool.Stray

© 2022 - 2024 — McMap. All rights reserved.