13 października 2011, 00:50 dario Komentarze (0)

MultiConstraint w ASP.NET MVC

Definiując ścieżki w aplikacji ASP.NET MVC możemy zadać warunki (constraint) określające poprawność reguły dla parametru. Ja na przykład stosuję (między innymi) domyślnie constraint, który przepuszcza ścieżki pisane tylko małymi literami. Dzięki temu ustrzegam się przed zdublowanymi stronami indeksowanymi przez wyszukiwarki (wielkość liter ma znaczenie).

Niestety dla jednego parametru można zadać tylko jeden warunek naraz. A co jeśli chcemy dołożyć kolejny? Można sobie pomóc korzystając z jednego, który przyjmuje kolejne. Tak oto mamy MultiConstraint.

public class MultiConstraint : IRouteConstraint
{
    private readonly IRouteConstraint[] _contraints;

    public MultiConstraint(params IRouteConstraint[] contraints)
    {
        _contraints = contraints;
    }

    public bool Match(
        HttpContextBase httpContext,
        Route route,
        string parameterName,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        return _contraints.All(x => x.Match(
            httpContext,
            route,
            parameterName,
            values,
            routeDirection));
    }
}
Zasada działania jest prosta. W konstruktorze podajemy dowolną ilość constraintów. Jeśli wszystkie będą spełnione to całość także.

Prosty przykład wywołania dla parametru controller:

controller = new MultiConstraint(new LowercaseConstraint(), new SubdomainConstraint(portalDomain))
Taka mała rzecz, a cieszy, bo bardzo mi brakowało takiej możliwości.

Tagi:

Asp.net-mvc

11 lipca 2011, 10:54 dario Komentarze (4)

Kompilacja widoków w ASP.NET MVC, ale na żądanie

Kompilowanie widoków zaoszczędza dużo czasu na sprawdzanie poprawności działania aplikacji. Niestety sam proces kompilacji wydłuża się i to znacznie. Dlatego szukałem sposobu, aby można było kompilować wybiórczo: z widokami lub bez. Niestety nie ma prostego przełącznika w Studio ani wbudowanego skrótu klawiaturowego.

Na szczęście trafiłem przypadkiem na stary już post pewnego Marka J. Millera, gdzie zwyczajnie pokazuje jak uzależnić kompilację widoków od konfiguracji trybu kompilacji. Studio skompiluje widoki wtedy, gdy w treści pliku csproj w sekcji PropertyGroups będziemy posiadali wpis:

<MvcBuildViews>true</MvcBuildViews>

W pliku csproj standardowo są 3 sekcje PropertyGroups. Wystarczy z domyślnej sekcji PropertyGroups usunąć powyższy wpis i wstawić odpowiednio tak jak poniżej:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <MvcBuildViews>false</MvcBuildViews>
    <...>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <MvcBuildViews>true</MvcBuildViews>
    <...>
</PropertyGroup>

Należy pamiętać, aby po tym zabiegu usunąć katalogi bin i obj z projektu webowego.

Dzięki takim zmianom podczas kompilacji w trybie DEBUG widoki nie będą kompilowane i kompilacja będzie szybka (w sam raz do debugowania). W trybie RELEASE widoki natomiast skompilują się dzięki czemu łatwiej jest wyłapać błędy składniowe w tych plikach. Szczególnie przydatne podczas zmian w modelach widoków, a już szczególnie kiedy oddajemy kod do repozytorium :)

Tagi:

Asp.net-mvc

6 grudnia 2010, 15:23 dario Komentarze (0)

Wykres MSChart i aktywna mapa

Poprzednim razem opisałem w jaki sposób wygenerować wykres za pomocą MSChart oraz w jaki sposób umiejętnie buforować wygenerowany obrazek w celu uniknięcia jego ciągłego pobierania przy każdym wejściu na stronę.

Teraz dodamy do tego generowanie aktywnej mapy i wyświetlenie jej na stronie. Dzięki temu będziemy mogli dodać do wykresu nieco życia. Po pierwsze możemy wykorzystać do tego standardowe dymki, które pokazuje przeglądarka kiedy zdefiniujemy tag title, czy dodać link do wybranych wartości (np.: w wykresie słupkowym otrzymaym klikalny słupek).

Wygenerowanie mapy

Jak się generuje sam wykres już wiecie. Teraz dodam do tego wygenerowanie mapy:

var chart = new Chart();
//...
string filename = "~/App_Code/charts/mychart.png".ToPhysical();
chart.SaveImage(fileName, ChartImageFormat.Png);

string imageMap = chart.GetHtmlImageMap("mychart");
File.WriteAllText(fileName.Replace(".png", ".map"), imageMap);

Powyższy kod tworzy wykres, zapisuje go do ustalonego katalogu, a zaraz po tym zapisuje się mapa w pliku o tej samej nazwie, ale rozszerzeniem map. Jak zatem uzyskać Tooltip czy "klikalny słupek"?

Dodawanie danych do wykresu

Dane dodaje się do kolekcji Serii. Podstawowym obiektem przetrzymującym dane jest DataPoint. Jak się przyjżeć bliżej to obiekt ten posiada także takie właściwości jak:

  • Tootip - czyli nasz dymek
  • Url - link, związany z naszymi wartościami
var series ...

var dayStart = DateTime.Today.AddDays(-daysAgo);
for (int i = 0; i <= 7; i++)
{
    var day = dayStart.AddDays(i);
    int perDay = ...

    var p = new DataPoint(
        day.ToOADate(),
        perDay);

    p.LabelForeColor = series.Color;

    p.ToolTip = "{0} - {1} {2}".FormatThis(
       day.ToString("yyyy-MM-dd"),
       series.Name,
       perDay.ToString());

    series.Points.Add(p);
}

Powyżej krótki przykład pokazujący w jaki sposób można dodać dane do serii. Kod dodaje obliczone ilości perDay na 7 dni wstecz od dzisiaj. Powstanie z tego na przykład ładny wykres słupkowy, gdzie na osi X będziemy mieli daty, a na osi Y wyliczone wartości.

Wyświetlenie mapy

Załóżmy, że wiemy, gdzie znajduje się plik map:

viewModel.GraphID = filename;
viewModel.GraphMap = System.IO.File
    .ReadAllText(filename.Replace(".png", ".map"));

W widoku umieszczamy wywołanie do wygenerowanego wykresu:

<%=Model.GraphMap %>
<%=Html.Image(Url.Action("graph", null, new { usemap = "#mychart", id = "chart" })%>

Należy zwrócić uwagę na tag usemap. Tag ten mówi przeglądarce, że dla danego obrazka ma użyć mapy o id "mychart". Nazwę taką podaliśmy wywołując metodę generującą mapę GetHtmlImageMap.

Podsumowanie

Tak wspominałem w poprzeniej notce, tworzenie wykresów MSChart nie jest trudne i można osiągnąć ciekawe efekty. W połączeniu z możliwościami MVC można je generować na żądanie nie obciążając zbytnio systemu.

Tagi:

Asp.net-mvc

12 października 2010, 10:16 dario Komentarze (0)

Wykres MSChart, ASP.NET MVC i cache

Od dłuższego czasu Microsoft udostępnia kontrolkę do generowania różnego rodzaju wykresów. Możliwości kontrolki są naprawdę bardzo duże. Dzięki niej można wygenerować naprawdę ładne wykresy.

Wykres

Jak wiadomo w aplikacji MVC kontrolka ta nie zadziała w taki sam sposób jak w aplikacji WebFormsowej. Tu nie można zwyczajnie położyć kontrolki na formie, gdyż wymaga ona PostBack'a. Musimy stworzyć obiekt samemu i go skonfigurować w kodzie. Na szczęście MSCharts ma zaimplementowane szablony, dzięki czemu w łatwy sposób (bez rekompilacji) można zmieniać wygląd wykresów.

var chart = new Chart();
//...
string filename = "~/App_Code/charts/mychart.png".ToPhysical();
chart.SaveImage(fileName, ChartImageFormat.Png);

Jak widać stworzenie wykresu jest proste. Trzy kropki należy wypełnić kodem, który skonfiguruje nasz wykres i wypełni danymi. Przykłady publikuje sam Microsoft.

Cache

No dobra, ale jak odwołać się do tego wykresu i gdzie ten cache? Już odpowiadam. Aby wykres zaistniał na stronie musimy się po niego odwołać. W tym celu tworzymy akcję kontrolera, która nam go zwróci.

public ActionResult Graph()
{
    if (UserContext.LoggedUser == null)
    {
        return PageNotFound();
    }

    string filename = ChartHelper.GetFileNameFor(UserContext.LoggedUser);
    if (!System.IO.File.Exists(filename))
    {
        // Nie było wczesniej, więc generujemy wykres
    }
    return new FilePathResultWith304(filename, "image/png");
}

Wywołanie z widoku wygląda następująco.

    <%=Html.Image(Url.Action("graph"), "Wykres") %>

Jak pewnie zauważyliście akcja zwraca FilePathResultWith304. Jest to klasa, która wysyła kontent wraz z nagłówkiem "If-Modified-Since". Dzięki temu kolejne odwołanie sprawdza ten nagłówek i porównuje z datą ostatniej modyfikacji pliku. Jeśli wykres nie jest nowszy to do przeglądarki wysyłany jest kod 304 Not modified i zero bajtów :)

public class FilePathResultWith304 : ActionResult
{
    private const string ifModifiedSince = "If-Modified-Since";

    public FilePathResultWith304(string fileName, string contentType)
    {
        FileName = fileName;
        ContentType = contentType;
    }

    public string FileName { get; private set; }
    public string ContentType { get; private set; }

    public override void ExecuteResult(ControllerContext context)
    {
        var response = context.HttpContext.Response;
        response.Clear();
        response.BufferOutput = false;
        response.ContentType = ContentType;

        if (!File.Exists(FileName))
        {
            response.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
            return;
        }

        var fi = new System.IO.FileInfo(FileName);
        var lastWriteTime = fi.LastWriteTimeUtc.Date
            .AddHours(fi.LastWriteTimeUtc.Hour)
            .AddMinutes(fi.LastWriteTimeUtc.Minute)
            .AddSeconds(fi.LastWriteTimeUtc.Second);
        var request = context.HttpContext.Request;

        var smodifiedSince = request.Headers[ifModifiedSince];
        if (!smodifiedSince.IsNullOrEmpty())
        {
            var modSince = smodifiedSince.Split(';');
            DateTime modifiedSince;
            if (modSince.Length > 0 && DateTime.TryParse(
                modSince[0], null,
                System.Globalization.DateTimeStyles.AdjustToUniversal,
                out modifiedSince))
            {
                if (modifiedSince.CompareTo(lastWriteTime) >= 0)
                {
                    response.Cache.SetCacheability(System.Web.HttpCacheability.Public);
                    response.Cache.SetLastModified(lastWriteTime);
                    response.Cache.SetMaxAge(TimeSpan.Zero);
                    response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
                    response.StatusDescription = "Not Modified";
                    return;
                }
            }
        }
        response.Cache.SetCacheability(System.Web.HttpCacheability.Public);
        response.Cache.SetLastModified(lastWriteTime);
        response.Cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
        response.Cache.SetMaxAge(TimeSpan.Zero);
        response.TransmitFile(FileName);
    }
}

Zabieg ten daje nam jeszcze jedną zaletę. Załóżmy, że następuje stosowna zmiana danych aplikacji, która powinna wygenerować nowy wykres. Na pierwszy rzut oka sposób postępowania mógłby być taki, aby zmienić dane w transakcji i zaraz potem wygenerować nowy wykres. Niestety wydłuży to czas takiej operacji, ale możemy odwlec moment wygenerowania wykresu dopóki ktoś po niego nie poprosi. W związku z tym po zmianie danych wystarczy na koniec usunąć plik wykresu z dysku. Akcja Graph wykona sprawdzenie dostępności pliku i w przypadku braku wygeneruje nowy.

Podsumowując

Jak widać tworzenie wykresów wcale nie jest trudne, a "ludzie biznesu bardzo lubią" takie kolorowe wykresiki, które wzbogacają aplikację. Kolejnym razem będzie o tym jak dodać aktywną mapę do danych wykresu wygenerowanego w powyższy sposób. :)

Tagi:

Asp.net-mvc

21 września 2010, 02:00 dario Komentarze (2)



Bardzo prosty sposób na skomunikowanie dwóch aplikacji MVC, czyli REST i JSON

To co napisałem poniżej to narazie koncept, który w sumie już działa, ale wymaga jeszcze nieco szlifu. Potrzebowałem skomunikować ze sobą dwie aplikacje webowe oparte o ASP.NET MVC. Naczytałem się o WCF i innych sposobach komunikacji (na przykład Webservice) i zawsze oznaczało to dosyć sporą ilość dłubania (a to jakieś atrybuty, a to opasłe xml'e konfiguracyjne). Zresztą jak dobrze poszukać to znajdą się opinie, że budowanie osobnych serwisów WCF wewnątrz aplikacji ASP.NET MVC, która sama z natury doskonale nadaje się do wystawiania w ten sposób danych jest bez sensu. Dlatego też wpadł mi do głowy pomysł, aby nieco podrasować kontrolery (czyli nasz Webservice ;)) oraz dobudować do tego prosty mechanizm klienta, który będzie się odwoływał do naszego nowego serwisu. Zależało mi przede wszystkim na jak największej prostocie użycia oraz użyciem przyjętych konwencji zamiast opasłych konfiguracji.

MVC

Kontroler aplikacji ASP.NET MVC musi zwracać obiekt typu ActionResult, aby mógł być zinterpretowany i wysłany do klienta w odpowiednim formacie. Jeśli obiektem zwracanym przez akcję będzie na przykład bool to w odpowiedzi dostaniemy treść w postaci "System.Boolean". Nastąpi automatyczna konwersja ToString i wysłanie treści typu text/html. W tym, celu stworzyłem specjalny ActionInvoker. W przypadku gdy akcja zwróci obiekt innego typu niż ActionResult to odpowiednio zinterpretuje dane i je zserializuje do formatu o jaki prosił klient. Zdefiniujmy zatem nasz serwis:

 

public interface ITestService : IControllerService
{
    bool Test(TestMessage msg);
}

 

IControllerService to tak zwany "marker" oznaczający nasz kontroler (potrzebny do wewnętrzengo działania ActionInvokera). Klasa wiadomości jest prosta no i musi być seralizowalna:

[Serializable]
public class TestMessage
{
    public string Value { get; set; }
}

Przykładowy kod kontrolera będącego serwisem:

[CompressFilter]
public class TestServiceController : Controller, ITestService
{
    [AcceptVerbs(HttpVerbs.Post)]
    public void bool Test(TestMessage msg)
    {
        Debug.WriteLine(msg.Value);
        return msg.Value != null;
    }
}

To już prawie wszystko. Teraz jeszcze trzeba podstawić do kontrolera nasz magiczny ActionInvoker. Można to zrobić na różne sposoby. Najprostszy to wykonać podstawienie w konstruktorze (patrz poniżej) lub dla bardziej wymagających, można posłużyć się StructureMap i podczas tworzenia kontrolera użyć TypeInterceptor.

[CompressFilter]
public class TestServiceController : Controller, ITestService
{
    public TestServiceController()
    {
        ActionInvoker = new ControllerServiceActionInvoker();
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public void bool Test(TestMessage msg)
    {
        Debug.WriteLine(msg.Value);
        return msg.Value != null;
    }
}

W tym momencie to już koniec tworzenia naszego serwisu. Banalne, prawda? :) Zwróćcie uwagę, że kontroller w pełni implementuje nasz interface ITestService. Interface ten będzie naszym tzw. kontraktem przy tworzeniu proxy po stronie klienta. Oznacza to, że wyciągamy nasz interface do zewnętrznego projektu Api (assembly), aby móc dodać do niego referencję w aplikacji klienckiej. Dzięki temu zmiana interfejsu pociągnie za sobą potrzebę zmiany kodu klienta jak i serwera, aby była cały czas zachowana spójność.

Klient

Teraz strona klienta. Załóżmy, że mamy już referencję do naszego Api. Tworzymy zatem proxy naszego serwisu.

ITestService srv = ServiceFactory.Create<ITestService>();

Koniec. :) Żartuję oczywiście, ale w tym momencie mamy już stworzony serwis, dzięki któremu możemy wywoływać metody poprzez nasze Api do zdefiniowanej powyżej akcji w aplikacji ASP.NET MVC. Wywołajmy zatem akcję.

bool result = srv.Test(new TestMessage(){ Value = "test"; });

Chciałem znów napisać, że to już koniec, ale się powstrzymam ;) W linii powyżej następuje:

  • serializacja obiektu TestMessage do Json (domyślna serializacja, można podstawiać własne serializery)
  • wysłanie Json'a POST'em (domyślny typ akcji, można zmieniać w zależności od potrzeby)
  • deserialziacja Json'a (w serwisie) do obiektu TestMessage jako argumentu wejściowego akcji Test
  • wykonanie kodu akcji na kontrolerze
  • serializacja wyniku akcji do Json'a
  • odebranie nowego Json'a u klienta
  • deserializacja Json'a do spodziewanego typu, który zwraca metoda Test, czyli System.Boolean

Wszystkie te działania są opakowane w wewnętrzną implementację, której końcowy użytkownik/programista wogóle nie musi być świadomy. Bo po co? :) Padnie pewnie pytanie: Skąd klient wie, gdzie strzelać requesty? Odpowiedź jest prosta: Convention over configuration, ale od początku. Na serwerze musi być zdefiniowana ścieżka (Route) do akcji naszego serwisu. Powiedzmy:

routes.MapRoute(
    "Api-Test",
    "api/test/{action}",
    new { controller = "testservice", action = "index" });

W związku z czym (na lokalnej maszynie) klient powinien strzelać pod adres:

http://localhost/nasza_aplikacja/api/test/{action}

I dokładnie powyższą linijkę wklejam w config naszego klienta w następujący sposób:

<appSettings>
    <add key="ITestServiceUri" value="http://localhost/nasza_aplikacja/api/test/{action}"/>
</appSettings>

Konstrukcja klucza jest banalna: "nazwa interfejsu naszego serwisu" + "Uri".

FactorySettings

Dodatkowo naszą fabryką można troszkę posterować z poziomu kodu. Wystaczy do metody Create podstawić obiekt ServiceFactorySettings:

var settings = new ServiceFactorySettings();
settings.Credientals = new NetworkCredential("a", "b");
settings.Serializer = new XmlDefaultSerializer();
settings.ServiceUri = new System.Uri("https://test.pl/api/test/{action}");

ITestService srv = ServiceFactory.Create<ITestService>(settings);

Z powyższego wynika, że można dać własne Credientials, programowo podstawić Uri oraz inny Serializer, który zresztą ma bardzo banalny interface:

public interface ISerializer
{
    object Deserialize(string value, Type returnType); //Deserializuje odebrane dane
    string Serialize(object value); //Serializuje dane do wysłania
    Encoding Encoding { get; } // Zwraca w jaki sposób dane mają być kodowane
    string ContentType { get; } // Zwraca typ kontentu, np.: application/json
}

Obecna implementacja jaką do tej pory stworzyłem posiada dwa wbudowane serializery:

  • JsonDefaultSerializer - korzysta z JavaScriptSerializer
  • XmlDefaultSerializer  - korzysta z XmlSerializer (ten jeszcze nie działa po stronie serwera :))

FormsAuthentication

Cały mechanizm pozwala na bardzo łatwe zbudowanie autentykacji w oparciu o FormsAuthentication. Kod klienta:

var srv = ServiceFactory.Create<ITestService>();
bool result = srv.Login(new LoginFormMessage() { Username = "a", Password = "b" });

Kod akcji kontrolera z użyciem wbudowanej metody CreateFormsAuthenticationSession

:

public bool Login(LoginFormMessage form)
{
    // walidacja użytkownika

    // powiedzmy, że wszystko jest ok
    this.CreateFormsAuthenticationSession();
    return true;
}

Wywołanie innej metody na tej samej instancji serwisu po stronie klienta:

bool result2 = srv.AuthorizedTest();

wykona poprawnie poniższą (w sumie bardzo standardową) akcję kontrolera, która wymaga autoryzacji:

[Authorize]
[AcceptVerbs(HttpVerbs.Post)]
public bool AuthorizedTest()
{
    return true;
}
 

Akcje - REST

Domyślnie klient wykonuje akcje POST'em, ale możemy go oczywiście odpowiednio wysterować opisując metody interfejsu następującymi atrybutami:

  • ExecuteName("test2") - podmienia nazwę akcji, zamiast domyślnej akcji będącej notabene nazwą metody wykona akcję test2
  • ExecutePost - http POST
  • ExecuteGet - http GET (w tym przypadku argumenty metody muszą być typami prostymi, gdyż zostaną przesłane jako QueryString)
  • ExecutePut - http PUT
  • ExecuteDelete - http DELETE

Przykład:

public interface ITestService : IControllerService
{
    [ExecuteAction("test2")]
    bool Test();

    [ExecutePut]
    [ExecuteAction("testvoid")]
    void TestVoidPut(string value);

    [ExecuteDelete]
    [ExecuteAction("testvoid")]
    void TestVoidDelete(string value);
}
 

Troszkę techniki

ControllerServices (nazwa kodowa) po stronie klienta swoje działanie opiera o Castle.DynamicProxy. Po stronie serwera (kontrolera) to tylko zabawa w podmianę domyślnego zachowania. Ogólne założenie jest takie, że serwis odpowiada takim typem kontentu o jaki został zapytany, czyli jeśli wysyłamy do serwisu zapytanie typu "application/json" to serwis odpowiada takim samym. Jeśli wysyłamy "text/xml" to serwis odpowiada "text/xml", itp, itd. Jeśli chcemy wysyłać własny format to piszemy własny Serializer. Podstawiamy do fabryki klienta oraz rejestrujemy w ActionInvokerze po stronie kontrolera. PS
Zwróćcie także uwagę na atrybut CompressFilter klasy TestServiceController. Nasz Json przelatuje z serwisu do klienta skompresowany Gzip'em. :) Oznacza to ni mniej ni więcej, że nasz "Webservice" działa jak najzwyczajniejszy kontroler aplikacji ASP.NET MVC. :)  

Teraz to już naprawdę koniec ;)

Wydaje mi się, że "prostota" rozwiązania została osiągnięta. Dajcie znać jak to widzicie. Jeśli macie jakieś uwagi, pomysły to z chęcią posłucham/przeczytam :)   Kod źródłowy wrzucę na Codeplex niebawem, ale do tego czasu przydała by się jakaś fajniejsza nazwa dla projektu. ;) Kod źródłowy można pobrać z codeplex.

Tagi:

Asp.net-mvc

O autorze

Dariusz Gil - projektant i programista aplikacji internetowych budowanych na platformie Microsoft w technologii ASP.NET (C#) oraz MS SQL Server. Obecnie właściciel (narazie :)) jednoosobowej firmy Softio.

Filtruj używając APML