本地化和缓存 - 更改文化时缓存失效

时间:2014-02-03 15:51:21

标签: asp.net-mvc asp.net-mvc-4 caching localization

假设我想要更改应用程序的UI文化。在索引视图中,我有一些负责更改语言的超链接:

<a href="@Url.Action("SetCulture", "System", new { lang = "en", returnUrl = Request.RawUrl }, null)">
    <span>English</span>
</a>
<a ...

如果我点击其中一个,我会被重定向到SetCulture操作,根据所选语言,会创建适当的文化并将其保留到会话(以及Cookie)中:

public class SystemController : Controller
{
    public ActionResult SetCulture(string lang, string returnUrl)
    {
        SetCultureToCookie(lang);
        return Redirect(returnUrl);
    }        
}

就在我们被重定向到我们理想的行动之前(例如/Home/Index):

public class HomeController : BaseController
{
    public ActionResult Index()
    {
        return View();
    }
    ...

执行流程首先通过OnActionExecuting事件。这里,先前存储到会话(和cookie)的文化被提取并分配给当前线程:

public abstract class BaseController : Controller
{        
    protected override void Initialize(System.Web.Routing.RequestContext requestContext)
    {
        base.Initialize(requestContext);

        // btw: thread culture should be setup as soon as possible (OnActionExecuting is too
        // late, because error messages of attributes applied to models will not be translated)
        var culture = GetCultureFromCookie();
        Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = culture;
    }
}

完成此操作后,我们必须最终找到合适的视图。视图层次结构在这里很简单:

/Views
    /Home
        - Index.cshtml
        - Index_en.cshtml
        ...

在视图渲染之前,视图引擎会搜索视图。我写了一些简单的自定义引擎,在Application_Start注册:

protected void Application_Start()
{
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new LocalizedRazorViewEngine());
    ...

根据当前的文化,它会查找相应的视图名称:

public class LocalizedRazorViewEngine : RazorViewEngine
{
    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        useCache = false;   // <--- NOTICE THAT

        var culture = Thread.CurrentThread.CurrentUICulture.Name;
        var isoLang = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;

        var locViewName = !string.IsNullOrEmpty(viewName) ? string.Format("{0}_{1}", viewName, culture) : viewName;
        var locMasterName = !string.IsNullOrEmpty(masterName) ? string.Format("{0}_{1}", masterName, culture) : masterName;

        var result = base.FindView(controllerContext, locViewName, locMasterName, useCache);
        if (result.View == null)
        {
            locViewName = !string.IsNullOrEmpty(viewName) ? string.Format("{0}_{1}", viewName, isoLang) : viewName;
            locMasterName = !string.IsNullOrEmpty(masterName) ? string.Format("{0}_{1}", masterName, isoLang) : masterName;

            result = base.FindView(controllerContext, locViewName, locMasterName, useCache);
            if (result.View == null)
                result = base.FindView(controllerContext, viewName, masterName, useCache);
        }

        return result;
    }
    ...

如果我选择加载英语(/System/SetCulture?lang=en&returnUrl=%2FHome%2FIndex),则上述方法会返回Index_en.cshtml视图。

一切正常但有一个缺点 - 这里有没有缓存(我假设默认情况下没有启用缓存,因为我正在使用VS2012并使用默认ASP创建我的应用程序的骨架。 NET MVC 4模板)。正如您所看到的,在FindView方法的开头,我已将useCache设置为false,因此视图引擎不会尝试查找任何缓存的视图 - 它将始终执行完整搜索。如果我删除此不良行并发布此应用程序到托管服务器,本地化不起作用 - 语言更改无效,一些旧视图始终呈现(即,如果我为索引视图选择英语,不会呈现Index_en.cshtml,而是前一个,例如Index.cshtmlIndex_fr.cshtml

如果您想在开发环境中重现此问题,则应该在发布模式下编译应用程序,并在<compilation debug="false" />中设置web.config,因为MVC不会如果在调试模式下运行应用程序,请执行任何视图查找缓存。

我有一些问题:

  1. 如何在我的情况下正确使用缓存?如何使它无效 语言改变了吗?
  2. 视图引擎级别(useCache)上的缓存如何与此不同 控制器/操作的[OutputCache]属性表示一个?如何 在这里也以正确的方式使用后者吗?
  3. BTW:正如你所看到的,我想避免这样一种机制,即当前语言由url中的永久前缀表示(例如mypage.com/en/Index),因为我不喜欢它。

    此致

2 个答案:

答案 0 :(得分:0)

这个问题似乎要求更高,因为我没有得到任何帮助,所以我会自己回答。 {MV}框架内部FindViewFindPartialView方法首先调用useCache=true,然后useCache=false调用public class LocalizedRazorViewEngine : RazorViewEngine { private static readonly Dictionary<string, string> _localizedViewsCache = new Dictionary<string, string>(); private static readonly object _locker = new object(); public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache) { viewName = FindLocalization(controllerContext.RouteData.Values["controller"].ToString(), viewName); return base.FindView(controllerContext, viewName, masterName, useCache); } public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache) { partialViewName = FindLocalization(controllerContext.RouteData.Values["controller"].ToString(), partialViewName); return base.FindPartialView(controllerContext, partialViewName, useCache); } private string FindLocalization(string controllerName, string viewName) { var isoLangName = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName; var cacheKey = String.Format("{0}/{1}_{2}", controllerName, viewName, isoLangName); if (_localizedViewsCache.ContainsKey(cacheKey)) return _localizedViewsCache[cacheKey]; var cultureName = Thread.CurrentThread.CurrentUICulture.Name; var localizedViews = new[] { string.Format("{0}_{1}", viewName, cultureName), string.Format("{0}_{1}", viewName, isoLangName) }; string foundViewName = null; foreach (var view in localizedViews) { foreach (var location in ViewLocationFormats) { if (VirtualPathProvider.FileExists(String.Format(location, view, controllerName))) { foundViewName = view; break; } } if (foundViewName != null) break; } viewName = foundViewName ?? viewName; lock (_locker) { if (_localizedViewsCache.ContainsKey(cacheKey)) _localizedViewsCache[cacheKey] = viewName; else _localizedViewsCache.Add(cacheKey, viewName); } return viewName; } } 。我的视图引擎实现被破坏了。出于某种原因,当启用缓存时,引擎服务于先前缓存的视图。这就是为什么我需要禁用缓存,以便始终强制搜索适当的视图。

现在我重写了视图引擎逻辑。我无法确切地说出前一个版本出了什么问题,但是当前版本能够正确支持基于当前UI文化的本地化视图缓存。

实施方式如何?首先,扫描磁盘以验证是否存在所需的视图文件 - 如果存在,则将其添加到缓存中(在进一步请求期间,直接从我们的自定义缓存返回此类文件名)。接下来,如果找到了这样一个适当的本地化视图名称,它将被传递给ASP.NET MVC框架,以便进一步缓存并正确地为任何后续请求提供服务:

[OutputCache(Location = OutputCacheLocation.Server, Duration = 3600, VaryByCustom = "culture")]
public class HomeController : BaseController

我还决定引入第二种类型的缓存来加速我的应用程序并使用输出缓存来标记其输出将被缓存的操作方法(或整个控制器)。您必须记住语言检测机制在我的方法中不依赖于URL模式。这就是为什么我需要实现一些负责根据当前UI文化提供不同视图的差异机制,而URL本身保持不变。我决定使用框架提供的VaryBy工具,这是一种指示ASP.NET保持同一页面的并行缓存因某些数据而异的方法。它可以是querystring或完全不同的东西。在我的例子中,它是存储在cookie中的文化名称:

Global.asax

public override string GetVaryByCustomString(HttpContext context, string custom) { if ("culture".Equals(custom)) { var cookie = HttpContext.Current.Request.Cookies.Get("myapp.culture.name"); var culture = cookie != null ? CultureInfo.CreateSpecificCulture(cookie.Value).Name : Guid.NewGuid().ToString(); // if culture cookie doesn't exist, do not rely // on cache - generate random guid to bypass it return culture; } return base.GetVaryByCustomString(context, custom); }

{{1}}

对于每个唯一参数(此处,存储在cookie中的文化值),ASP.NET将保留先前在该参数下请求的此类页面的缓存副本。

答案 1 :(得分:0)

您可以包装 ViewLocationCache 。例如:

public class LocalizedRazorViewEngine : RazorViewEngine
{
    public LocalizedRazorViewEngine()
    {
        ViewLocationCache = new LocalizedViewLocationCache(ViewLocationCache);
    }
}

public class LocalizedViewLocationCache : IViewLocationCache
{
    public LocalizedViewLocationCache(IViewLocationCache innerCache)
    {
        _innerCache = innerCache ?? DefaultViewLocationCache.Null;
    }

    public string GetViewLocation(HttpContextBase httpContext, string key)
    {
        return _innerCache.GetViewLocation(httpContext, generateCacheKey(key));
    }

    public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath)
    {
        _innerCache.InsertViewLocation(httpContext, generateCacheKey(key), virtualPath);
    }

    private string generateCacheKey(string key)
    {
        return string.Format(":LocalizedViewCacheEntry:{0}:{1}",
            Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName, key);
    }

    private readonly IViewLocationCache _innerCache;
}