ASP.NET 核心自定义验证会创建新的模型实例



我正在玩 ASP.NET Core,并试图为一个简单的文字游戏想出一个UI。您会收到一个随机生成的长单词,并且您需要从长单词提供的字母中提交较短的单词。

该应用程序尚不使用任何类型的存储库,它现在只是将模型实例存储为控制器中的静态字段。

我目前面临一个问题,每次验证新提交的单词时,都会创建一个新的游戏实例,这自然会保证呈现验证错误,因为每个游戏都提供一个新的长单词。

我一定误解了模型验证的工作方式,但调试并没有给我更好的线索,而不仅仅是显示每次都带有一个新的长词的验证上下文。

我被困住了,请帮忙。

下面是控制器:

public class HomeController : Controller
{
private static WordGameModel _model;
public IActionResult Index()
{
if (_model == null)
{
_model = new WordGameModel();
}
return View(_model);
}
[HttpPost]
public IActionResult Index(WordGameModel incomingModel)
{
if (ModelState.IsValid)
{
_model.Words.Add(incomingModel.ContainedWordCandidate);
return RedirectToAction(nameof(Index), _model);
}
return View(_model);
}
}

游戏模式:

public class WordGameModel
{
public WordGameModel()
{
if (DictionaryModel.Dictionary == null) DictionaryModel.LoadDictionary();
LongWord = DictionaryModel.GetRandomLongWord();
Words = new List<string>();
}
public string LongWord { get; set; }
public List<string> Words { get; set; }
[Required(ErrorMessage = "Empty word is not allowed")]
[MinLength(5, ErrorMessage = "A word shouldn't be shorter than 5 characters")]
[MatchesLettersInLongWord]
[NotSubmittedPreviously]
public string ContainedWordCandidate { get; set; }
public bool WordWasNotSubmittedPreviously() => !Words.Contains(ContainedWordCandidate);
public bool WordMatchesLettersInLongWord()
{
if (string.IsNullOrWhiteSpace(ContainedWordCandidate)) return false;
return ContainedWordCandidate.All(letter => LongWord.Contains(letter));
}
}

验证失败的自定义验证属性:

internal class MatchesLettersInLongWord : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
WordGameModel model = (WordGameModel) validationContext.ObjectInstance;
if (model.WordMatchesLettersInLongWord()) return ValidationResult.Success;
return new ValidationResult("The submitted word contains characters that the long word doesn't contain");
}
}

视图:

@model WordGameModel
<div class="row">
<div class="col-md-12">
<h2>@Model.LongWord</h2>
</div>
</div>
<div class="row">
<div class="col-md-6">
<form id="wordForm" method="post">
<div>
<input id="wordInput" asp-for="ContainedWordCandidate"/>
<input type="submit" name="Add" value="Add"/>
<span asp-validation-for="ContainedWordCandidate"></span>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-md-6">
<ul>
@foreach (var word in @Model.Words)
{
<li>@word</li>
}
</ul>
</div>
</div>

谢谢。

您的视图需要包含LongWord的隐藏输入,以便在 POST 方法中,以便在 ModelBinder 调用构造函数后,根据表单值(即您发送到视图的值(设置LongWord

<form id="wordForm" method="post">
<div>
<input type="hidden" asp-for="LongWord" /> // add hidden input
<input id="wordInput" asp-for="ContainedWordCandidate"/>
<input type="submit" name="Add" value="Add"/>
<span asp-validation-for="ContainedWordCandidate"></span>
</div>
</form>

作为旁注,在您的 post 方法中,它应该只是return RedirectToAction(nameof(Index));- GET 方法没有(也不应该(具有模型的参数,因此传递它没有意义(无论如何它只会创建一个丑陋的查询字符串(

不要在控制器中使用静态字段来存储单词。将状态保留在控制器中不是一个好主意,因为如另一个答案中所述,控制器是transient的,并且为每个请求创建一个新控制器。因此,即使静态变量仍然可用,将其与控制器一起使用也不好。此外,您希望保持模型干净,即不要在其中放入任何业务/游戏逻辑。为此使用不同的类。仅使用模型来确保值有效,即最小长度、所需等。

针对您的问题的更好解决方案是创建一个singleton服务来存储数据。作为单一实例,在应用程序的生存期内只会创建一个服务。您可以使用依赖关系注入将其注入控制器,并将其用于每个请求,因为知道对于每个请求,它将是相同的服务实例。

例如:

public interface IWordService
{
IEnumerable<String> Words { get; }
bool WordWasNotSubmittedPreviously(string word);
bool WordMatchesLettersInLongWord(string longWord, string containedWordCandidate);
void AddWordToList(string word);
}
public class WordService : IWordService
{
private List<string> _words;
public IEnumerable<string> Words => _words;
public WordService()
{
_words = new List<string>();
}
public bool WordWasNotSubmittedPreviously(string containedWordCandidate) => !_words.Contains(containedWordCandidate);
public bool WordMatchesLettersInLongWord(string longWord, string containedWordCandidate)
{
if (string.IsNullOrWhiteSpace(containedWordCandidate)) return false;
return containedWordCandidate.All(letter => longWord.Contains(letter));
}
public void AddWordToList(string word)
{
_words.Add(word);
}
}

此服务完成您的ValidationAttribute所做的所有工作,但我们可以使用依赖项注入来确保我们只为整个应用程序创建一个。

在你Startup.cs将其添加到ConfigureServices方法中:

public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IWordService, WordService>();
....
}

现在我们可以将其注入到我们的控制器中,并且由于我们已将其注册为singleton因此每次都会获得相同的实例,即使我们获得控制器的不同实例也是如此:

public class HomeController : Controller
{
private readonly IWordService _wordService;
public HomeController(IWordService wordService)
{
_wordService = wordService;
}
[HttpPost]
public IActionResult Index(WordGameModel incomingModel)
{
if (ModelState.IsValid)
{
// Use the `_wordService instance to perform your checks and validation
...
}
...
}
}

我已经将_wordService的实际用途留给您来实现:-(但它应该相当简单。

您可以在此处阅读有关依赖注入 (DI( 的更多信息

还有这里的ConfigureServices方法

对于HomeController中对操作的每个请求,mvc 框架都会为此创建控制器的新实例。返回响应后,它将释放控制器。

控制器字段和对象不能在请求之间共享。在您调用每个操作的情况下,您的WordGameModel将再次实例化,并且它的构造函数会创建一个新的 Long 单词。
您可以将对象保存在某个数据库中,以便每个用户提供该功能。

最新更新