允许用户选择性地覆盖您网站的默认日期
一种低影响的方法,通过使用 Session 变量 + 自定义 Action 过滤器,允许最终用户选择他们想要的测试日期,从而覆盖 Web 请求期间使用的默认日期。
引言
我开发的一个 ASP.NET MVC 网站需要为演示目的进行准备,以便展示过去可用的视图。此外,未来最好有一种简便的方法来进行历史日期用户测试。我创建了一个名为 VRSandbox
的侧边项目,它是实现此目的的概念验证。它依赖于用户在客户端浏览器中设置的 Session
变量,用于所需的测试或演示日期。这使得更改测试日期变得容易,并且不同的用户可以同时指定不同的日期。引入此功能基本上归结为向控制器和/或控制器操作添加一个自定义过滤器,并对需要测试的视图模型进行少量修改。
背景
此项目是为支持我的另一个更大的项目 Voter's Revenge(以下简称 VR)而编写的。请参阅 votersrevenge.org 和 votersrevenge.info。简而言之,Voter's Revenge 的任务是通过促进惩罚性投票集团的形成,使公民能够增长民粹主义政治力量。破坏性的、惩罚性的投票集团(尽管它们可能太小/太弱而无法“聘请”其首选的替代者,但却拥有“解雇”现任者的政治影响力)在各种改革力量因冷漠和士气低落、缺乏组织等原因而薄弱的国家尤为重要。例如在美国,所有反对通过民主党+共和党“两党制”所体现的 the plutocratic status quo 的改革派,发现自己不断被边缘化、剥夺权力,和/或被收买。
我在 2016 年夏天暂停了 VR 的积极开发。在那之后不久,程序数据库中的所有美国竞选活动都进入了最后的阶段。为了在未来有用,需要加载新的数据,并添加编程更改以无缝处理新的竞选活动。(此外,还需要引入用于 CRUD 操作的体面的界面。)总而言之,这项工作量相当大。
我初步计划将 VR 开源。为了让程序显示一个有趣的视图,包含其现有数据和(几乎不变的)代码,我需要一种方法来欺骗程序,使其能够显示关于一个历史日期的视图,该日期远在大多数初选结束之前。这也有助于程序的未来测试人员。
VRSandbox(这个 Visual Studio 解决方案的名称)因此成为了 VR 的一个 ASP.NET MVC 侧边项目。我相信我为 VRSandbox 开发的代码对其他可能希望启用历史日期测试但又不想对现有代码库造成太大干扰的开发者来说是有用的。
Using the Code
要使用该代码,需要类似地修改数据模型,就像我将我的 CampaignModel
类扩展到 AssignableDateCampaignModel
类一样。关键步骤是:
- 安装 NodaTime(通过 nuget)并在需要的地方添加命名空间引用。
- 在类中添加一个
private
成员来存储 NodaTime 的LocalDate
变量(在我的例子中是“assignedLocalDate
”)。 - 修改您预先存在的
DateTime
字段的 getter,以便在已设置时返回此LocalDate
变量(转换为DateTime
)。 - 创建一个公共方法来设置
private
LocalDate
变量。在我的例子中,我添加了一个包含LocalDate
值的构造函数,以及一个AssignLocalDate
方法。
这是我的视图模型
public class CampaignModel
{
public string CandidateName { get; set; }
public DateTime QueryDateTime
{
get
{
// return midnight UTC
return DateTime.UtcNow.Date.ToUniversalTime();
}
}
}
public class AssignableDateCampaignModel : CampaignModel
{
private LocalDate assignedLocalDate = new LocalDate();
public AssignableDateCampaignModel(string candidateName, LocalDate assignedLocalDate)
{
this.assignedLocalDate = assignedLocalDate;
this.CandidateName = candidateName;
}
public DateTime QueryDateTime
{
get
{
// if we didn't assign a query date (stored as assignedLocalDate)
if (assignedLocalDate == new LocalDate())
// return midnight UTCNow
return DateTime.UtcNow.Date.ToUniversalTime();
else
{
// return midnight of LocalDate
return assignedLocalDate.ToDateTime();
}
}
}
public void AssignLocalDate(LocalDate assignedLocalDate)
{
this.assignedLocalDate = assignedLocalDate;
}
}
此外,编码人员必须
- 添加相关文件,如 /Utils/*.*。
- 将
SessionQueryDate
自定义过滤器类添加到 /App_Start/FilterConfig.cs。 - 将自定义过滤器属性 [
SessionQueryDate
] 添加到您希望使用已分配日期的控制器或操作。 - 提供一些方法供最终用户将“
sessionLocalDate
” Session 变量设置为他们选择的日期。在 VRSandbox 中,我修改了示例项目的代码。如何创建和访问 ASP.NET MVC 中的会话变量
SessionQueryDate
操作过滤器的代码
public class SessionQueryDate : ActionFilterAttribute, IActionFilter
{
// set ViewBag.TestQueryLocalDate if parse successful
// otherwise, set it to null
public void OnActionExecuting(ActionExecutingContext context)
{
LocalDate validLocalDate;
var sessionLocalDate = context.HttpContext.Session.Contents["sessionLocalDate"];
if (sessionLocalDate == null)
{
context.Controller.ViewBag.TestQueryLocalDate = null;
}
else
{
bool tryParsing = ((string)sessionLocalDate).IsValidLocalDate(out validLocalDate);
if (tryParsing)
{
context.Controller.ViewBag.TestQueryLocalDate = validLocalDate;
}
else
{
context.Controller.ViewBag.TestQueryLocalDate = null;
}
}
}
}
这是两个使用 [SessionQueryDate
] 的操作的代码。其中第一个是 GetResults()
,它使用了 AssignableDateCampaignModel
。由于该模型具有 AssignLocalDate()
方法,因此使用 SessionQueryDate
中设置的 LocalDate
值调用它,将导致用户选择的测试日期用于涉及 AssignableDateCampaignModel.QueryDateTime
的查询。
[SessionQueryDate]
public ViewResult GetEvents()
{
ViewBag.Title = "GetEvents() - view model has an assignable date";
ViewBag.Assignable = true;
// 2 example models
// ============================
// CampaignModel does not have an assignable date
// CampaignModel campaignModel = new CampaignModel { CandidateName = "Donald J. Trump" };
// AssignableDateCampaignModel, used here in GetEvents() does have an assignable date
AssignableDateCampaignModel campaignModel =
new AssignableDateCampaignModel("Hillary Clinton", new LocalDate());
LocalDatePattern pattern0 = LocalDatePattern.CreateWithInvariantCulture("MM-dd-yyyy");
// ViewBag.TestQueryLocalDate is set in the SessionQueryDate Action Filter
if (ViewBag.TestQueryLocalDate != null)
{
// assign the LocalDate to the model
LocalDate localDate = ViewBag.TestQueryLocalDate;
// add to model
campaignModel.AssignLocalDate(localDate);
// return the same value in the session variable
ViewData["sessionLocalDate"] = pattern0.Format(localDate);
// update status
ViewData["sessionLocalDateStatus"] = "sessonLocalDate session variable set";
}
else
{
// return the same (bad format or empty) value
ViewData["sessionLocalDate"] = null; // as String;
// update status
ViewData["sessionLocalDateStatus"] = "sessonLocalDate session variable not set";
}
// construct the view model using .QueryDateTime
List<MyEvent> events = MyEvents.Where
(x => x.EventDate >= campaignModel.QueryDateTime).ToList();
return View(events);
}
第二个示例操作也调用了 campaignModel.AssignLocalDate()
。尽管 campaignModel
的类型是 CampaignModel
,并且没有可分配日期的功能,但 NodaTimeUtils
类中定义了一个同名的扩展方法,因此代码仍然可以编译。在这种情况下,AssignLocalDate
实际上什么也没做。
[SessionQueryDate]
public ViewResult GetEventsNoAssignableDate()
{
ViewBag.Title = "NodaSessionStateController.GetEventsNoAssignableDate()
- view model does not have an assignable date";
// 2 example models
// ============================
// CampaignModel does not have an assignable date
CampaignModel campaignModel =
new CampaignModel { CandidateName = "Donald J. Trump" };
//// AssignableDateCampaignModel does have an assignable date
//AssignableDateCampaignModel campaignModel =
//new AssignableDateCampaignModel("Hillary Clinton", new LocalDate());
LocalDatePattern pattern0 =
LocalDatePattern.CreateWithInvariantCulture("MM-dd-yyyy");
// ViewBag.TestQueryLocalDate is set in the SessionQueryDate Action Filter
if (ViewBag.TestQueryLocalDate != null)
{
// assign the LocalDate to the model
LocalDate localDate = ViewBag.TestQueryLocalDate;
// add to model, where appropriate
// since the current model has no assignable date,
// AssignLocalDate() will not do anything to the model
campaignModel.AssignLocalDate(localDate);
// return the same value in the session variable
ViewData["sessionLocalDate"] = pattern0.Format(localDate);
// update status
ViewData["sessionLocalDateStatus"] = "sessonLocalDate session variable set";
}
else
{
// return the same (bad format or empty) value
ViewData["sessionLocalDate"] =
System.Web.HttpContext.Current.Session["sessionLocalDate"] as String;
// update status
ViewData["sessionLocalDateStatus"] =
"sessonLocalDate session variable not set";
}
// construct the view model using .QueryDateTime
List<MyEvent> events = MyEvents.Where
(x => x.EventDate >= campaignModel.QueryDateTime).ToList();
return View("GetEvents", events);
}
屏幕截图
包含所有数据的首页
提交前分配日期
提交无效日期后
提交有效日期后
使用已分配日期的查询结果
查询结果忽略已分配日期(执行于 2017 年 3 月 9 日)
注释
- 使用了 NodaTime,但对其用途并不真正需要。NodaTime 在处理时区时非常有用,但在我的应用程序中,
Date
的粒度就足够了。如果您不打算处理时区,应该可以轻松地将代码重构为仅使用 .NET 的DateTime
库。 - 我已将日期格式硬编码为“
MM-dd-yyyy
”。 - 用 [
SessionQueryDate
] 装饰整个控制器是可以的,因为使用不具有可分配日期的模型的那些操作不会中断。
我的开发环境
我在 Visual Studio 2013 Ultimate 中开发,使用 .NET framework 4.5.2(因此是 C# 6)。“数据库”只是一个静态集合。
历史
- 2017 年 3 月 15 日:初始版本