自动处理空模型以清理控制器和视图 [英] Automate handling of null models to clean up controllers and views

查看:62
本文介绍了自动处理空模型以清理控制器和视图的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我真的很想通过减少我正在执行的空检查数量来提高代码的当前可读性.我觉得尽管检查某些条件是一件好事,但在各个地方重复进行这些检查不是一个好主意.例如,这是我的PostsController的一种方法:

public ActionResult ShowPost(int PostID, string slug)
{
    PostViewModel viewModel = new PostViewModel();
    Post model = postRepository.FindPost(PostID, filterByPublished: true);

    if (model.PostID == 0)
        return Redirect(Url.Home());
    else if (model.Slug != slug)
        return RedirectPermanent(Url.ShowPost(model.PostID, model.Slug));

    postRepository.PostVisited(model);
    Mapper.Map(model, viewModel);

    return View(viewModel);
}

我不喜欢的东西

首先,检查PostID是否为0.由于我在存储库中设置该方法的方式,因此它可以为0:

public Post FindPost(int id, bool filterByPublished = false)
{
    var query = db.Posts.Where(post => post.PostID == id);

    if (filterByPublished)
        query = query.Where(post => post.IsPublished == filterByPublished);

    return query.Select(post => post).SingleOrDefault() ?? new Post { PostID = 0 };
}

我不喜欢在那儿放一些小技巧,只是为了满足处理null模型的需要.我还在各种需要PostViewModel的强类型视图中检查空模型.

可能的解决方案

我的第一个想法是创建一个动作过滤器,覆盖OnResultExecuting并在那里检查空模型.我实际上很喜欢这个想法,它确实解决了在控制器和视图中检查空模型的问题.但是,它不能满足以下情况:

else if (model.Slug != slug)

这将给我一个空引用异常,就像将模型传递给postRepository来更新视图计数一样.我猜想对Mapper.Map的调用会起到同样的作用.

那我该怎么办?我可以简单地覆盖OnActionExecuted并在那里检查异常,将其记录下来,然后重定向到自定义错误视图吗?听起来像是一种合理的方法吗?

谢谢.

解决方案

好,让我们尝试将此控制器置于节食.

我们首先修复您的存储库,然后让它返回null而不是ID = 0的一些默认帖子,这就像您注意到的那样很奇怪:

public Post FindPost(int id, bool filterByPublished = false)
{
    var query = db.Posts.Where(post => post.PostID == id);

    if (filterByPublished)
        query = query.Where(post => post.IsPublished == filterByPublished);

    return query.Select(post => post).SingleOrDefault();
}

然后我们可以为Post模型编写一个自定义模型活页夹:

public class PostModelBinder : IModelBinder
{
    private readonly IPostsRepository _repository;
    public PostModelBinder(IPostsRepository repository)
    {
        _repository = repository;
    }

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var postIdValue = controllerContext.Controller.ValueProvider.GetValue("postid");
        int postId;
        if (postIdValue == null || !int.TryParse(postIdValue.AttemptedValue, out postId))
        {
            return null;
        }

        return _repository.FindPost(postId, true);
    }
}

可能与Application_Start中的Post类型相关联:

var postsRepository = DependencyResolver.Current.GetService<IPostsRepository>();
ModelBinders.Binders.Add(typeof(Post), new PostModelBinder(postsRepository));

,然后是一个自定义动作过滤器,当帖子为空或帖子子句与作为参数传递的帖子不同时,它将小心地缩短该动作:

public class PostActionFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var postParameter = filterContext
            .ActionDescriptor
            .GetParameters()
            .Where(p => p.ParameterType == typeof(Post))
            .FirstOrDefault();
        if (postParameter == null)
        {
            return;
        }

        var post = (Post)filterContext.ActionParameters[postParameter.ParameterName];
        if (post == null)
        {
            filterContext.Result = new RedirectResult(Url.Home());
        }

        var slug = filterContext.Controller.ValueProvider.GetValue("slug");
        if (slug != null && post.Slug != slug.AttemptedValue)
        {
            filterContext.Result = new RedirectResult(
                Url.ShowPost(post.PostID, post.Slug), 
                true
            );
        }
    }
}

如果您使用的是ASP.NET MVC 3,则该自定义属性可以在Global.asax中注册为全局属性:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new PostActionFilterAttribute());
    ...
}

最后,您的控制器动作将变为:

public ActionResult ShowPost(Post post)
{
    postRepository.PostVisited(post);
    var viewModel = Mapper.Map<Post, PostViewModel>(post);
    return View(viewModel);
}

或者甚至更进一步,并引入自定义的映射器动作过滤器:

[AutoMap(typeof(Post), typeof(PsotViewModel))]
public ActionResult ShowPost(Post post)
{
    postRepository.PostVisited(post);
    return View(post);
}

AutoMap操作过滤器非常易于实现.您将订阅OnActionExecuted方法,并检查从控制器操作返回的结果是否为ViewResultBase,然后从中提取模型,将其传递给给定两种类型的AutoMapper,然后将模型替换为视图模型. /p>

I'd really like to improve the current readability of my code by reducing the amount of null checks I'm doing. I get the feeling that although checking for certain conditions is a good thing, repeating those checks in various places isn't such a good idea. As an example, here's one method of my PostsController:

public ActionResult ShowPost(int PostID, string slug)
{
    PostViewModel viewModel = new PostViewModel();
    Post model = postRepository.FindPost(PostID, filterByPublished: true);

    if (model.PostID == 0)
        return Redirect(Url.Home());
    else if (model.Slug != slug)
        return RedirectPermanent(Url.ShowPost(model.PostID, model.Slug));

    postRepository.PostVisited(model);
    Mapper.Map(model, viewModel);

    return View(viewModel);
}

What I don't like

Firstly, the check to see if PostID is 0. It can be 0 because of the way I set that method up in the repository:

public Post FindPost(int id, bool filterByPublished = false)
{
    var query = db.Posts.Where(post => post.PostID == id);

    if (filterByPublished)
        query = query.Where(post => post.IsPublished == filterByPublished);

    return query.Select(post => post).SingleOrDefault() ?? new Post { PostID = 0 };
}

I don't like that I've pushed that little hack in there just to cater for handling a null model. I also check for a null model in various strongly-typed views which require a PostViewModel.

Possible solution

My first thought would be to create an action filter, override OnResultExecuting and check for null models there. I actually quite like this idea and it does solve the problems of checking for null models in the controller and also the view. However, what it doesn't do is cater for situations like this:

else if (model.Slug != slug)

That'll give me a null reference exception as would passing the model to the postRepository to update the view count. I'm guessing the call to Mapper.Map would do the same.

So what can do I about it? Could I simply override OnActionExecuted and check for exceptions there, logging them and then redirecting to a custom error view? Does that sound like a reasonable approach?

Thank you.

解决方案

OK, let's try putting this controller on a diet.

We start by fixing your repository and leaving it return null instead of some default post with ID = 0 which was kind of weird as you noticed:

public Post FindPost(int id, bool filterByPublished = false)
{
    var query = db.Posts.Where(post => post.PostID == id);

    if (filterByPublished)
        query = query.Where(post => post.IsPublished == filterByPublished);

    return query.Select(post => post).SingleOrDefault();
}

then we could write a custom model binder for the Post model:

public class PostModelBinder : IModelBinder
{
    private readonly IPostsRepository _repository;
    public PostModelBinder(IPostsRepository repository)
    {
        _repository = repository;
    }

    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var postIdValue = controllerContext.Controller.ValueProvider.GetValue("postid");
        int postId;
        if (postIdValue == null || !int.TryParse(postIdValue.AttemptedValue, out postId))
        {
            return null;
        }

        return _repository.FindPost(postId, true);
    }
}

which could be associated with the Post type in Application_Start:

var postsRepository = DependencyResolver.Current.GetService<IPostsRepository>();
ModelBinders.Binders.Add(typeof(Post), new PostModelBinder(postsRepository));

and then a custom action filter that will take care to short-circuit the action in case the post is null or the post slug is different than the one passed as parameter:

public class PostActionFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var postParameter = filterContext
            .ActionDescriptor
            .GetParameters()
            .Where(p => p.ParameterType == typeof(Post))
            .FirstOrDefault();
        if (postParameter == null)
        {
            return;
        }

        var post = (Post)filterContext.ActionParameters[postParameter.ParameterName];
        if (post == null)
        {
            filterContext.Result = new RedirectResult(Url.Home());
        }

        var slug = filterContext.Controller.ValueProvider.GetValue("slug");
        if (slug != null && post.Slug != slug.AttemptedValue)
        {
            filterContext.Result = new RedirectResult(
                Url.ShowPost(post.PostID, post.Slug), 
                true
            );
        }
    }
}

If you are using ASP.NET MVC 3 this custom attribute could be registered as a global one in Global.asax:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new PostActionFilterAttribute());
    ...
}

and finally your controller action will become:

public ActionResult ShowPost(Post post)
{
    postRepository.PostVisited(post);
    var viewModel = Mapper.Map<Post, PostViewModel>(post);
    return View(viewModel);
}

or even bringing this a step further and introducing a custom mapper action filter:

[AutoMap(typeof(Post), typeof(PsotViewModel))]
public ActionResult ShowPost(Post post)
{
    postRepository.PostVisited(post);
    return View(post);
}

The AutoMap action filter is pretty easy to implement. You would subscribe to the OnActionExecuted method and check whether the returned result from the controller action was a ViewResultBase and then extract the model from it, pass it to AutoMapper given the 2 types and substitute the model with the view model.

这篇关于自动处理空模型以清理控制器和视图的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆