Url.Action does not work in CMS 12?

Vote:
 

Hi!

We have just upgraded to CMS 12 and we have this in our view:

<form id="searchbox" action="@Url.Action("Index", "SearchPage")" method="get" autocomplete="off" class="page-searchbox tracked-search">

In CMS 11 I get this: 

<form id="searchbox" action="/sv/admin/soksida" method="get" autocomplete="off" class="page-searchbox tracked-search">

which is correct, but in CMS 12 I get this:

<form id="searchbox" action="/modules/App/SearchPage" method="get" autocomplete="off" class="page-searchbox tracked-search">

What am I missing?

Thanks!

/Kristoffer

#286055
Aug 24, 2022 9:07
Vote:
 

UPDATE:

I added:

_ = endpoints.MapControllerRoute(name: "Default", pattern: "{controller}/{action}/{id?}");

and that gave me:

<form id="searchbox" action="/SearchPage/Index" method="get" autocomplete="off" class="page-searchbox tracked-search">

Still missing something.

Come to think aobut it, I think it is more strange that it works in CMS 11? Shouldn't 

action="@Url.ContentUrl(Model.CurrentPage.ContentLink)"

be tha way to go?

/Kristoffer

#286061
Edited, Aug 24, 2022 11:10
Vote:
 

Kristoffer I was looking into this myself and believe its due to the way the Middle Layer for routing has changed in .Net Core. As you mentioned it might be better switching to Url.ContentURL or if you have a view model at your disposle store the URL in a property. 

@{ using (Html.BeginForm(null, null, Html.ViewContext.IsInEditMode() ? FormMethod.Post : FormMethod.Get, new { @action = Model.Layout.SearchActionUrl }))
    {
        <input type="text" class="search-query" name="q" id="SearchKeywords" placeholder="@Html.Translate("/mainnavigation/search")" />
        <input type="submit" class="searchButton" id="SearchButton" value="" />
    }
}

Or 

        <div class="searchContainer" id="searchBar">
            <form class="searchWrap" method="get" action="@Url.ContentUrl(Model.SearchResultsPage)#results-top">

                <div class="searchForm">
                    <label for="searchSite" class="offscreen">Search site</label>
                    <input type="text" id="searchSite" maxlength="50" name="@Configuration.Constants.QueryString.Query" placeholder="@Model.SearchBoxPlaceholderText" />
                    <button type="submit" class="icon searchButton">
                        <svg>
                            <title>@Model.SearchText</title>
                            <use xlink:href="#svg-search"></use>
                        </svg>
                    </button>
                </div>
            </form>
#286065
Aug 24, 2022 13:39
Vote:
 

Hi Minesh!

Yes, I will solve it in some of the ways above, it will work just fine. I just wanted to know that is does not work and that I'm not just missed to add something.
I will start like that and we will see.

Thanks!

/Kristoffer

#286067
Aug 24, 2022 16:59
Vote:
 

The reason is becasue of how middleware works and the order of exceution changed between the net 5/ 6 and full framework.  Using contenturl is the way to go.

#286068
Aug 24, 2022 21:09
Vote:
 

Have you tried using @Html.BeginContentForm instead? I think it's a new addition to CMS 12.  It has a lot of methods so I created an extension method to simplify it in a couple of places:

public static class HtmlHelperExtensions
{
    public static MvcForm BeginContentForm<T>(
        this IHtmlHelper<T> htmlHelper, 
        ContentReference? contentReference,
        FormMethod formMethod,
        string formClass)
    {
        return htmlHelper.BeginContentForm(
            contentReference, 
            CultureInfo.CurrentUICulture.Name, 
            null,
            formMethod, 
            false, 
            new { @class = formClass });
    }
}

Then on the razor file I have this:

@using (Html.BeginContentForm(Model.SiteSettings.SearchPage, FormMethod.Get, "footer-search__form"))
{
    <div class="p-footer__input-group">
        <input type="text"
               name="query"
               placeholder="@Model.SiteSettings.FooterSearchPlaceholder"
               aria-label="@Model.SiteSettings.FooterSearchPlaceholder"
               autocomplete="off"
               required />
        <button type="submit" class="footer-search__button">Search</button>
    </div>
}
#286171
Aug 26, 2022 8:02
Vote:
 

I'm running into this as well for Url.Action no longer working with content urls. I think the best bet in many situations may be to inject IUrlResolver in your view and call getUrl with VirtualPathArguments as you can set Action on it. Perhaps a UrlHelper extension might be a good idea for syntactic sugar.

Unfortunately, there are cases were link generation might not be in your complete control, so I would love to see content aware implementations of IUrlHelper and LinkGenerator

#288260
Sep 28, 2022 20:51
Vote:
 

I'm experiencing this issue as well - @Url.Action(...) creates the MVC route rather than the content route.

What puzzles me is that Episerver Foundation tries to achieve this as well:

https://github.com/episerver/Foundation/blob/main/src/Foundation/Features/Checkout/Checkout.cshtml#L79

https://github.com/episerver/Foundation/blob/main/src/Foundation/Features/Checkout/CheckoutController.cs#L786

#288628
Oct 04, 2022 13:13
Vote:
 

@Quan, can you please add this? You're literally doing the exact same thing in RedirectToContentResultExecutor. Also, while you're at it - can you add RouteValues to RedirectToContent/RedirectToContentResult and add it to the URL generator in RedirectToContentResultExecutor? :) Should be a easy thing for you, that makes life easier for us!

#291040
Nov 03, 2022 9:07
Vote:
 

This is an interesting problem we have encountered too while upgrading a solution to the v12. How do you handle the query parameters when using Url.ContentUrl? Do you prefer to write them by hand or is there any extension method that can generate the link with query parameters given an anonymous object?

#296682
Feb 17, 2023 10:08
Vote:
 

I've built the following helper methods to replace Url.Action and supply additional route values - it's worked out well so far

    public static class UrlHelperExtensions
    {
        public static string ContentAction(this IUrlHelper urlHelper, string action)
        {
            return ContentAction(urlHelper, action, null, null, null);
        }

        public static string ContentAction(this IUrlHelper urlHelper, string action, object values)
        {
            return ContentAction(urlHelper, action, null, null, values);
        }

        public static string ContentAction(this IUrlHelper urlHelper, string action, ContentReference contentLink, string language, object values)
        {
            var routeValues = values as RouteValueDictionary ?? new RouteValueDictionary(values);
            routeValues["action"] = action;
            return ContentUrl(urlHelper, contentLink, language, routeValues);
        }

        public static string ContentUrl(this IUrlHelper urlHelper, ContentReference contentLink, string language, object values)
        {
            var routeValues = values as RouteValueDictionary ?? new RouteValueDictionary(values);
            return ContentUrl(urlHelper, contentLink, language, routeValues);
        }

        public static string ContentUrl(this IUrlHelper urlHelper, ContentReference contentLink, string language, RouteValueDictionary routeValues)
        {
            if (ContentReference.IsNullOrEmpty(contentLink) || string.IsNullOrEmpty(language))
            {
                var feature = urlHelper.ActionContext.HttpContext.Features.Get<IContentRouteFeature>();
                if (feature != null)
                {
                    var routedContentData = feature.RoutedContentData;
                    language = string.IsNullOrEmpty(language) ? routedContentData.RouteLanguage : language;
                    contentLink = ContentReference.IsNullOrEmpty(contentLink) ? routedContentData.Content?.ContentLink : contentLink;
                }
            }

            if (!ContentReference.IsNullOrEmpty(contentLink))
            {
                var action = routeValues["action"] as string;

                routeValues.Remove("controller");
                routeValues.Remove("action");
                routeValues.RemoveEmptyValues();

                var arguments = new VirtualPathArguments {
                    Action = action,
                    RouteValues = routeValues
                };

                var resolver = urlHelper.ActionContext.HttpContext.RequestServices.GetRequiredService<UrlResolver>();
                return resolver.GetUrl(contentLink, language, arguments);
            }

            return string.Empty;
        }
}

    public static class IDictionaryExtensions
    {
        public static void RemoveEmptyValues<TKey, TValue>(this IDictionary<TKey, TValue> source)
        {
            var keys = source.Keys.ToList();
            foreach (var key in keys) {
                var value = source[key] as object;
                if (value == null ||
                    (value is string s && string.IsNullOrEmpty(s)) ||
                    (value is StringValues sv && StringValues.IsNullOrEmpty(sv)))
                {
                    source.Remove(key);
                }
            }
        }
    }
#296720
Edited, Feb 17, 2023 14:54
huseyinerdinc - Feb 17, 2023 15:16
Great, thanks!
lander - Sep 25, 2024 21:07
We have been using the ContentAction solution provided by Matthew Jimenez and even though it seemed to work out in the first few months. A problem has arisen in the meantime.

When adding action methods to a pageController for example, it's possible that the default aspnetcore.routing will suddenly confuse your action method with the main index method on that pageController (it uses some built in scoring to rank the pageController methods & then picks the first one with score >= 0 ).
Note:
1) the sort order of the methods can change all of a sudden if a piece of code is added elsewhere in the codebase.
2) the sort order of the methods is not always the same order for all developers that run the same code locally.

This blog post suggests to fix this using some kind of priority attribute: https://world.optimizely.com/forum/developer-forum/cms-12/thread-container/2022/11/migrating-partial-classes-of-same-controller-to-optimizely-12/ ... But it's probably best to just always move your action methods to a separate Controller & not putting them on the content controller anymore. So, in retrospect... there's a reason Url.Action doesn't work for content routes anymore...

Hope to save someone some time ;)
This topic was created over six months ago and has been resolved. If you have a similar question, please create a new topic and refer to this one.
* You are NOT allowed to include any hyperlinks in the post because your account hasn't associated to your company. User profile should be updated.