Pull to refresh

Фотосайт на MVC 4 для начинающих

Reading time9 min
Views22K
Bongiozzo Photosite

Если у кого-то есть желание сделать свой фотосайт или познакомиться с ASP.NET MVC на примере, возможно, пригодится мой опыт создания такого сайта и исходники лягут в основу собственного проекта.

Чтобы было понятно о чем пойдет речь, ссылка на сайт — www.bongiozzo.ru.

А теперь, собственно, как это было сделано.

Что хотелось?
  • Сделать портфолио в виде нескольких фотоальбомов и добавить контекст из соц сетей;
  • Хотелось сделать что-то свое — я изначально не смотрел на готовые решения;
  • В качестве хранилища фотографий сразу планировал использовать существующие сервисы фотохостинга с активным сообществом фотографов — у сервисов есть API, кроме того, фотографии публикуются для обратной связи, а получить ее можно, прежде всего, там;
  • По той же причине, прикрутить к фотографиям кнопки Facebook и Twitter;
  • Для просмотра фотографий планировалось обзавестись стильной галереей, которая будет хорошо выглядеть на планшетах;
  • Ну и в завершении, хотелось все это выложить на оптимальный, с точки зрения соотношения стоимость/сервис, хостинг.

Задача поставлена, поехали…



Определяемся со средой разработки и framework

Первым делом свободно скачал и установил Visual Studio 2012 RC. Для разработки веб сайтов доступны Web Forms и MVC — мои симпатии всецело лежат на стороне MVC, т.к. этот паттерн использовал для создания сайтов на Perl, когда ещё Web Forms и в помине не было :).
По поводу версии MVC тоже особых раздумий не было — раз уж все новое (VS 2012 на Windows 8), то и разрабатывать буду на последней версии MVC 4.

Главное — контент

Pro аккаунт на Flickr у меня уже был, хотя это абсолютно необязательное условие. Автоматическая группировка и сортировка фотографий по альбомам на Flickr у меня уже тоже была настроена с помощью сервиса SmartSetr. Поэтому первым делом открыл Nuget Package Manager и, введя Flickr, получил в проекте поддержку Flickr API c помощью библиотеки Flickr.NET.
Возможно позднее рассмотрю перенос CMS на 500px, а может кто-то это сделает быстрее меня.

Идея использования Flickr как CMS была простой — фотоальбомы с ключевым словом Project в описании будут отображаться на моем сайте (во Flickr они называются Photoset). Фотографии в альбоме будут отсортированы по популярности, определяемой пользователями Flickr.

Вот какие несложные методы получились для модели формирования данных FlickrModel.

        public List<FlickrProject> GetProjects(string language)
        {
            PhotosetCollection flickrPhotosets = flickr.PhotosetsGetList(Properties.Settings.Default.FlickrUserId);

            Regex reg_Project = new Regex(@"^Project\s+([^\|].*)\s+\|\s+(.*)");
            Match m;
            List<FlickrProject> returnProjects = new List<FlickrProject>();
            foreach (Photoset item in flickrPhotosets)
            {
                m = reg_Project.Match(item.Description);
                if (m.Success)
                {
                    FlickrProject prj = new FlickrProject();
                    prj.Description = getDescription(item.Description, language);
                    prj.PhotosetId = item.PhotosetId;
                    prj.Photos = GetPhotosetPhotos(item.PhotosetId, language);
                    prj.PrimaryPhoto = prj.Photos.ToList().Find(
                        delegate(Photo ph) 
                           { return ph.PhotoId == item.PrimaryPhotoId; });
                    returnProjects.Add(prj);
                }
            }
            return returnProjects;
        }

        public PhotosetPhotoCollection GetPhotosetPhotos(string photosetId, string language)
        {
            PhotosetPhotoCollection photos = flickr.PhotosetsGetPhotos(photosetId,  PhotoSearchExtras.Description );
            foreach (Photo item in photos)
            {
                item.Title = getTitle(item.Title, language);
                item.Description = getDescription(item.Description, language);
            }
            return photos;
        }


Не сложнее выглядят модели для последних публикаций Twitter и оценок на фильмы с Kinopoisk.

Если вы обратили внимание — метод возвращает названия и описания фотографий на разных языках.

Называть фотографии для своих или на английском?

Был у меня такой вопрос некоторое время назад. Я решил, что буду дублировать названия на русском и английском, разделяя их вертикальной чертой |. Теперь такое именование стало возможным использовать на сайте лучшим образом — если посетитель по умолчанию использует Русский в настройках браузера — показывать ему родные названия, иначе английские. В отличие от сайтов фотохостинга на моем сайте это будет выглядеть естественно, без всяких разделительных палок.

В MVC есть механизм фильтров, которые применяются ко всем или отдельным запросам. Для этой задачи был реализован фильтр LocalizationAware, который будет отрабатывать на запросах к основным страницам сайта и выставлять нужную языковую культуру в зависимости от настроек браузера:

    public class LocalizationAwareAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var httpContext = filterContext.HttpContext;
            string language = "en";
            HttpCookie langCookie = httpContext.Request.Cookies["lang"];

            if (langCookie == null)
            {
                if (httpContext.Request.UserLanguages != null &&
                    httpContext.Request.UserLanguages[0].IndexOf("ru", StringComparison.OrdinalIgnoreCase) != -1)
                    language = "ru";

                httpContext.Response.AppendCookie(createCookie(language));
            }
            else
                if (langCookie.Value == "ru")
                    language = "ru";

            Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(language);
        }

        public static HttpCookie createCookie(string language)
        {
            HttpCookie cookie = new HttpCookie("lang", language);
            cookie.Expires = DateTime.Now.AddYears(1);
            return cookie;
        }
    }


Конечно же, есть возможность поменять язык, если выбор по умолчанию не устраивает:

        public ActionResult setLanguage(string id)
        {
            id = id == "ru" ? "ru" : "en";
            Response.AppendCookie(LocalizationAwareAttribute.createCookie(id));
            return Redirect("/");
        }


Помимо названий фотографий, все элементы страницы — заголовки и тексты теперь тоже автоматически переключаются на выбранный язык благодаря выставленной языковой культуре и наличие в проекте двух ресурсных файлов — Resources.resx и Resources.ru.resx. Что-то кодировать дополнительно для этого не потребовалось — этот функционал встроенный.

Быстродействие и кэширование

Плюсы сетевых сервисов очевидны, однако эти преимущества будут сведены на нет, если отклик будет небыстрым, особенно в пиковые нагрузки. Очевидно, ответы от Flickr, Twitter и других систем требуется кэшировать.

Сделать это просто — достаточно подключить соответствующий фильтр, указав перед методом атрибут OutputCache.
Для формирования страницы у меня используется несколько Actions — основной запрос (Index, например) и несколько ChildActions для рендера частей страницы, содержащих ответы от разных внешних систем. Для каждого типа контента можно использовать свою продолжительность хранения в кэше, в зависимости от динамики изменений.

        [LocalizationAware]
        [OutputCache(Duration = 20 * 60, Location = System.Web.UI.OutputCacheLocation.Server, VaryByCustom = "lang")]
        public ActionResult Index()
        {
            return View(bag);
        }

        [ChildActionOnly]
        [OutputCache(Duration = 10*60*60)]
        public ActionResult GetProjects(string language)
        {
            HomeViewBag bag = new HomeViewBag();
            FlickrContext flickr = new FlickrContext();
            bag.FlickrProjects = flickr.GetProjects(language);
            return View(bag);
        }

        [ChildActionOnly]
        [OutputCache(Duration = 60 * 60)]
        public ActionResult GetLastPhotos(string language)
        {
            HomeViewBag bag = new HomeViewBag();
            FlickrContext flickr = new FlickrContext();
            bag.LastPhotos = flickr.GetLastPhotos(5, language, 1).ToList();
            return View(bag);
        }


ASP.NET MVC сам разделяет закэшированные копии результатов в зависимости от значений входных параметров. Для ChildActions входные параметры в виде языка явно определены, однако для основных запросов потребуется разделять кэш в зависимости от языка сессии и этот параметр явно не присутствует в параметрах. Это реализуется с помощью атрибута VaryByCustom = «lang» и специального метода GetVaryByCustomString. Теперь в кэше не должно быть наложений.

        public override string GetVaryByCustomString(HttpContext context, string arg)
        {
            if (arg.ToLower() == "lang")
            {
                string langParam = context.Request.QueryString["lang"];
                HttpCookie langCookie = context.Request.Cookies["lang"];

                if (langParam != null)
                    return langParam == "ru" ? "ru" : "en";
                else
                    if (langCookie != null)
                        return langCookie.Value == "ru" ? "ru" : "en";
                return "";
            }
            return base.GetVaryByCustomString(context, arg);
        }


Дизайн и фотогалерея

Т.к. задача изначально ставилась — сделать сайт самостоятельно, а сам я совсем не дизайнер, было сразу решено сделать вид сайта максимально простым и ограничиться выразительными способностями CSS3 и HTML5. Тем более, что на фотосайте основной акцент должен идти на фотографии и их подачу.

Поэтому я поискал доступные для использования галереи и остановился на стильном решении Galleria. Галерея написана на jQuery, имеет свой API, базовая редакция, достаточная для расширения — бесплатна и доступна для использования. То, что нужно.

Помимо незначительных корректировок в модуль инициализации галереи добавил свои элементы навигации и интеграцию с соц сетями:

        this.append({ 'stage': ['closeBG', 'socialBG'] });
        this.$('socialBG').html('<div id="div-fblike"></div><div id="div-twitter"></div>');     
        this.$('closeBG').bind("click", function (e) { window.location = "/"; });


Наполнение галереи фотографиями выглядит как заполнение массива в JSON формате данными, полученными из FlickrModel.

Формирование JSON в Controller:

        private string getJSON(List<Photo> list, string p)
        {
            List<PhotoJSON> json = new List<PhotoJSON>();
            foreach (Photo pht in list)
                json.Add(new PhotoJSON
                {
                    thumb = pht.ThumbnailUrl,
                    image = pht.LargeUrl,
                    title = pht.Title,
                    description = pht.Description,
                    link = pht.WebUrl,
                    url = Settings.Default.SiteURL + Url.Action("Gallery", new { controller = "Home", id = pht.PhotoId, photoset = p })
                });

            JavaScriptSerializer serializer = new JavaScriptSerializer();
            return serializer.Serialize(json);
        }



JavaScript во Viewer:

    var data = @Html.Raw(Model.PhotosJSON);

    Galleria.loadTheme('/galleria/themes/bongiozzo/galleria.bongiozzo.js');
    Galleria.run('#galleria', {
        show: @Model.IndexOfCurrentPhoto,
        transition: 'slide',
        initialTransition: 'fade',
        dataSource: data });


Вешаем твиты и лайки

На событие переключения фотографии в галерее вешаем обновление кнопок Facebook и Twitter.

        this.bind("image", function (e) {
            document.title = galleria.getData().title;

            $('#div-fblike').html('<fb:like href="' + data[galleria.getIndex()].url + '" layout="button_count" send="false" show_faces="false" colorscheme="dark" />');
            $('#div-twitter').html('<a href="https://twitter.com/share" class="twitter-share-button" data-url="' + data[galleria.getIndex()].url + '" data-text="' + galleria.getData().title + '">Tweet</a>');
            try {
                FB.XFBML.parse(document.getElementById('div-fblike'));
                twttr.widgets.load();
            } catch(ex) {}
        });


И не забываем заполнить OpenGraph теги для корректного отображения в Facebook и других OG совместимых системах.

    <meta property="fb:app_id" content="@Model.fb_app_id"/>
    <meta property="og:title" content="@ViewBag.Title" />
    <meta property="og:site_name" content="@Resources.Resources.Title" />
    <meta property="og:type" content="website" />
    <meta property="og:url" content="@Model.SiteUrl@Url.Action("Gallery", new { controller = "Home", id = Model.GalleryPhotos[Model.IndexOfCurrentPhoto].PhotoId, photoset = Model.PhotosetId})" />
    <meta property="og:image" content="@Model.GalleryPhotos[Model.IndexOfCurrentPhoto].SquareThumbnailUrl" />
    <meta property="og:description" content="@Html.Raw(Model.GalleryPhotos[Model.IndexOfCurrentPhoto].Description.Replace("\n","<br/>"))" />


Где хостить проект?

К моменту когда фотосайт можно было запускать, очень кстати стал доступен облачный хостинг Windows Azure с 10 бесплатными Shared сайтами. Есть отдельная стоимость за базу данных и траффик, однако, учитывая, что базы данных у меня нет и сами фотографии хранятся на Flickr, а, соответственно, нет и траффика — хостинг видится практически бесплатным. При этом, в случае c Azure, у меня нет вопросов касательно качества и доступности сервиса, что немаловажно. Управление сервисом понятное и простое — перед Azure я рассматривал хостинг от других провайдеров и это особенно бросилось в глаза на контрасте запутанного интерфейса, переписки и общения по телефону со службами поддержки.

Есть, к сожалению, одно НО — нельзя привязать свое доменное имя к этому Shared сайту. Для тех, кому собственное доменное имя пока не сильно нужно и устраивает URL типа bongiozzo.azurewebsites.net — это, действительно, оптимальный вариант.
Однако, есть надежда, что это временное ограничение для Shared сайтов. Поэтому пока я перевел сайт в режим Reserved, в котором такую привязку сделать можно. В любом случае пока работает 90 дневный Trial и у меня есть MSDN подписка с включенной предоплатой определенного количества сервисов Azure.

Выйдет следующая версия Azure Web Sites — буду смотреть, возможно переведу на минимальный хостинг Infobox.ru, Parking.ru и т.д., или XS экземпляр облачных служб, а пока работает, работает отлично и денег не стоит.

В качестве заключения

  • Проект оказался несложным — в самый раз для знакомства или разминки. Ушло с десяток вечеров на знакомство с ASP.NET MVC 4, CSS3, jQuery и Azure;
  • Исходники можно взять здесь — bongiozzo.codeplex.com;
  • Буду рад вопросам, конструктивным пожеланиям и комментариям — @bongiozzo;
Tags:
Hubs:
+12
Comments9

Articles

Change theme settings