Автоматизируйте обработку нулевых моделей для очистки контроллеров и представлений - PullRequest
1 голос
/ 22 января 2012

Я бы действительно хотел улучшить читаемость моего кода, уменьшив количество проверок на ноль, которые я делаю.У меня такое ощущение, что хотя проверка на определенные условия - это хорошая вещь, повторять эти проверки в разных местах - не очень хорошая идея.В качестве примера, вот один метод моего 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 };
}

Мне не нравится, что я запустил этот маленький хак, чтобы обслуживать нулевую модель.Я также проверяю нулевую модель в различных строго типизированных представлениях, для которых требуется PostViewModel.

Возможное решение

Моей первой мыслью было бы создать фильтр действий, переопределить OnResultExecuting ипроверьте на нулевые модели там.Мне на самом деле очень нравится эта идея, и она решает проблемы проверки нулевых моделей в контроллере, а также в представлении.Однако, что он не делает, так это обслуживает ситуации, подобные этой:

else if (model.Slug != slug)

Это даст мне исключение нулевой ссылки, как при передаче модели в postRepository для обновления счетчика просмотров.Я предполагаю, что звонок на Mapper.Map сделает то же самое.

Так что я могу с этим сделать?Могу ли я просто переопределить OnActionExecuted и проверить там исключения, заносить их в журнал и затем перенаправлять в пользовательский вид ошибок?Это звучит как разумный подход?

Спасибо.

1 Ответ

7 голосов
/ 23 января 2012

ОК, давайте попробуем поставить этот контроллер на диету .

Мы начнем с того, что исправим ваш репозиторий и оставим его возвращать ноль вместо некоторого поста по умолчанию с 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);
    }
}

, которое может быть связано с типом Post в Application_Start:

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 с данными 2 типами и заменили модель моделью представления.

...