【问题标题】:ASP.NET Core custom validation creates new instance of modelASP.NET Core 自定义验证创建模型的新实例
【发布时间】:2018-10-25 16:40:40
【问题描述】:

我正在使用 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>

谢谢。

【问题讨论】:

    标签: c# asp.net asp.net-mvc asp.net-core model-validation


    【解决方案1】:

    您的视图需要包含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 方法没有(也不应该)有模型的参数,因此传递它没有意义(它只会创建无论如何都是一个丑陋的查询字符串)

    【讨论】:

    • 这个!非常感谢。实际上,谢谢两次,因为我将在下一步解决查询字符串问题。
    • 我假设您将来会摆脱该静态字段并使用存储库:)
    • 我当然会!这只是一个存根,我试图一次解决一个问题。
    【解决方案2】:

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

    更好的解决方案是创建一个singleton 服务来存储数据。作为单例,在应用程序的整个生命周期中只会创建一个服务。您可以使用 Dependency Injection 将其注入到您的控制器中,并为每个请求使用它,因为它知道每个请求都将是同一个服务实例。

    例如:

    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) here

    还有ConfigureServices方法here

    【讨论】:

    • 谢谢。我有一个草稿服务,稍后会开始使用它,但现在,很遗憾,它并不能解决我眼前的问题。
    【解决方案3】:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-03-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-08-24
      相关资源
      最近更新 更多