Zwijanie oraz rozwijanie fragmentów strony www

Często na stronie www chcemy umieścić elementy, które powinny być domyślnie zwinięte aby zajmowały mniej miejsca. Na przykład na tej stronie zdecydowałem, że filtrowanie wyników powinno domyślnie być schowane aby zajmowało mniej miejsca. Klikając na link “Pokaż\Ukryj” użytkownik może rozwinąć okno. Warto również zwrócić uwagę na animacje – okno nie pojawia się od razu ale stopniowo jest rozszerzane.

Zaprezentowany wynik można łatwo uzyskać za pomocą biblioteki jQuery. Po ściągnięciu biblioteki oraz podłączeniu stosownych plików JS należy napisać następujący skrypt:

<script type="text/javascript">
    $(document).ready(function () {        
        $("#msg_body").hide();            
    });
</script>

Metoda hide (ukrycie formularza) zostanie wywoła w momencie zainicjowania biblioteki jQuery. Selektor #msg_body oznacza wybranie elementu w którym atrybut id jest równy msg_body. Następnie można przejść do zdefiniowania reszty formularza:

<%using(Html.BeginForm("Search","Politicians",FormMethod.Get)){ %>

<div id="msg_head">

    <a onclick='javascript:$("#msg_body").slideToggle(600);return false' href='#' class="marked_link">
        <img  src="/images/search.png" alt="Filtrowanie" style="float:left" />Filtrowanie (Pokaż\Ukryj)
    </a>

</div>
    
<br />
<div id="msg_body">
    <fieldset>

        <legend>Filtrowanie</legend>
        <p>Imię:
            <br />
            <%=Html.TextBox("FirstName", Request.QueryString["FirstName"]) %>
        </p>

        <p>Drugie imię:
            <br />
            <%=Html.TextBox("SecondName", Request.QueryString["SecondName"]) %>
        </p>

        <p>Nazwisko:
            <br />
            <%=Html.TextBox("LastName", Request.QueryString["LastName"]) %>
        </p>
        <input type=submit value="Filtruj" />
    </fieldset>
</div>
<%} %>

Element div z id równym msg_body zawiera dane, które będą zwijanie oraz rozwijane. Z kolei w msg_head został umieszczony link odpowiedzialny za wykonanie operacji. W zdarzeniu OnClick umieszczono następujący kod JS:

'javascript:$("#msg_body").slideToggle(600);return false'

Kluczem do sukcesu jest metoda slideToggle, która odpowiada za animację zwijania lub rozwijania. Jako parametr wejściowy można przekazać całkowity czas animacji.

Funkcja string.Trim a T-SQL

W .NET mamy do dyspozycji funkcję Trim (string.Trim) służącą do usuwania pustych znaków (spacji) zarówno przed jak i po stringu. Przykład:

string text="    jakiś tekst  ";
trimmedText=text.Trim();
// teraz trimmedText równy jest "jakiś tekst"

Jak widać spację zostały usunięte. W T-SQL nie mamy dokładnie takiej samej funkcji ale łatwo uzyskać identyczny efekt za pomocą LTRIM oraz RTRIM:

LTRIM(RTRIM(kolumna))

Najpierw usuwamy wszystkie znaki z prawej strony a potem dopiero z lewej. Można również napisać własną funkcję opakowującą powyższy kod.

ASP.NET MVC – obrazek jako link do asynchronicznego wywołania

W poprzednich postach pokazałem jak tworzyć asynchroniczne formularze. Nie zawsze jednak chcemy wywoływać takie zapytania za pomocą przycisku Submit. Czasami lepiej użyć linku tekstowego lub po prostu obrazka. Ajaxowy link tekstowy bardzo łatwo utworzyć za pomocą metody ActionLink:

<%=Ajax.ActionLink("Usuń","RemoveAssocation",new AjaxOptions(){}) %>

Pytanie jednak co zrobić aby obrazek posłużył jako ajaxowy link? W poście przedstawię może nieeleganckie ale za to bardzo proste rozwiązanie:

<%= Ajax.ActionLink("[replacethis]", ...).Replace("[replacethis]", "<img src=\"/images/remove.gif\" ... />" %> 

Najpierw generujemy zwykły, tekstowy link (<a href=’…’>[replacethis]</a>), a następnie wystarczy zamiast tekstu wstawić obrazek (metoda Replace). Dla porządku w kodzie można napisać rozszerzenie do AjaxHelpera:

static public class AjaxHelperEx
{
   public static string ImageActionLink(this AjaxHelper helper, string imageUrl, string altText, string actionName, object routeValues, AjaxOptions ajaxOptions)
   {
       var builder = new TagBuilder("img");
       builder.MergeAttribute("src", imageUrl);
       builder.MergeAttribute("alt", altText);
       string link = helper.ActionLink("[replaceme]", actionName, routeValues, ajaxOptions).ToHtmlString();            
       return link.Replace("[replaceme]", builder.ToString(TagRenderMode.SelfClosing));
   } 
}

Teraz w widoku wystarczy, że wywołamy:

<%=Ajax.ImageActionLink("/images/remove.png","Usuń","RemoveAssocation",null,new AjaxOptions(){}) %>

ASP.NET MVC – asynchroniczne wysyłanie formularzy (AJAX) na przykładzie prostego ShoutBox’a

Podobnie jak w czystym ASP.NET, framework ASP.NET MVC pozwala na wysyłanie formularzy w tle. Załóżmy, że chcemy napisać shoutbox’a, w którym wysłanie wiadomości na serwer nie wymagałoby ponownego przeładowania całej strony. Zamiast definiowania formularza za pomocą Html.BeginForm, musimy skorzystać z Ajax.BeginForm:

<% using(Ajax.BeginForm("AddMessage",new AjaxOptions(){ OnComplete="OnFinish", UpdateTargetId="messages"}))
{ %>
// treść
<%}%>

Pierwszy parametr to nazwa akcji. W drugim parametrze przekazujemy klasę AjaxOptions określającą między innymi zdarzenie w JavaScript wywoływane po zakończeniu zapytania oraz identyfikator elementu modyfikowanego w trakcie wykonywania akcji. Stwórzmy więc prosty ShoutBox:

<div  id="messages" style="width: 350px; height: 80px; overflow:auto;">
<%Html.RenderPartial("Messages"); %>
</div>
<%
   using(Ajax.BeginForm("AddMessage",new AjaxOptions(){ OnComplete="OnFinish", UpdateTargetId="messages"}))
   { %>

   <%if(Page.User.Identity.IsAuthenticated==false){ %>
   Nick:
   <br />
   <%=Html.TextBox("Nick", "", new { maxlength = 50, style = "width:220px" })%>
   
   <%} %>
   <br />
   Wiadomość:
   <br />
   <%=Html.TextBox("Message", "Wpisz opinię, która pojawi się na mównicy...", new { maxlength = 500, title = "Wpisz opinię, która pojawi się na mównicy...", @class = "water", style = "width:220px" })%>
   
   
   <input type="submit"  value="Wyślij" />


<%} %>

Najpierw definiujemy element div z ID ustawionym na “messages”. Następnie tworzymy asynchroniczny formularz za pomocą Ajax.BeginForm. Zawartość formularza nie zawiera już żadnej nowości – jedynie standardowe pola: jedno na nick a drugie na wiadomość. Przyjrzyjmy się jeszcze akcji AddMessage:

public ActionResult AddMessage(string message)
{
  Repositories.Shoutbox.ShoutboxRepository repository = new Repositories.Shoutbox.ShoutboxRepository();
  if(string.IsNullOrEmpty(message))
      return PartialView("Messages", repository.GetNewestMessages());

  Entities.ShoutBoxMessage shoutMessage = new Entities.ShoutBoxMessage() { Message = message };
  shoutMessage.ID_MESSAGE = Guid.NewGuid();
  shoutMessage.Date = DateTime.Now;

  if (User.Identity.IsAuthenticated == false)
  {
      if (!string.IsNullOrEmpty(Request["Nick"]))
      {
          shoutMessage.UserName = Request["Nick"];
      }
  }
  else
      shoutMessage.ID_USER = new Guid(System.Web.Security.Membership.GetUser().ProviderUserKey.ToString());

  repository.Create(shoutMessage);
  repository.SubmitChanges();            
  return PartialView("Messages", repository.GetNewestMessages());
}

Kod tworzy wiadomość (klasa ShoutBoxMessage) oraz dodaje ją do baz danych. Następnie akcja zwraca widok “Messages” z zawartością wszystkich wiadomości (repository.GetNewestMessages). Widok częściowy (PartialView) jest dodawany do elementu div (messages) w sposób asynchroniczny. Nie wymaga to oczywiście przeładowania strony. Na zakończenie jeszcze widok Messages.ascx (dodawany do wspomnianego już div):

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<PoliticiansPromises.Entities.ShoutBoxMessage>>" %>
<%@ Import Namespace="PoliticiansPromises.Extensions" %>
<% foreach(var item in Model){ %>

<%if(item.ID_USER==null){ 
      
      if(item.UserName==null){
      %>
      <span class="marked_link">~Anonim</span>
      <%}else{ %>
      <span class="marked_link">~<%:item.UserName %></span>
      <%} %>
<%}else{ %>


<span class="marked_link"><%:System.Web.Security.Membership.GetUser(item.ID_USER).UserName %></span>

<%} %>


<span class="small_gray_style"><%: String.Format("{0:g}", item.Date)%></span>
<%=Html.EncodeAndCut(item.Message,500) %>
<br />

<%} %>
<br />

T-SQL i NULLIF

Dzisiaj bardzo króciutki post. Do tabeli dodałem computed column (wartość liczona na podstawie innych kolumn). Formuła obliczająca wartość wyglądała następująco:

VotesFor / TotalVoteCount

Chciałem po prostu dodać kolumnę, która liczy procent oddanych głosów aby potem móc stworzyć indeks po tej kolumnie. W przypadku jednak gdy TotalVoteCount wynosi 0 pojawił się wyjątek Divide by Zero. Na szczęście funkcja NULLIF rozwiązała problem:

VotesFor / NULLIF(TotalVoteCount,0)

W sytuacji gdy TotalVoteCount równy jest 0, NULLIF zwróci wartość NULL i tym samym całość formuły zakończy się poprawnie z wynikiem również NULL. Proste rozwiązanie ale mam nadzieje, że komuś się przyda 🙂

ASP.NET MVC oraz sortowanie wyników po kolumnach

W jednym z poprzednich postów pokazałem jak zrealizować stronicowanie wyników. Kontrolka ListView z czystego ASP.NET miała jeszcze jedną zaletę – możliwość sortowania wyników po kolumnach. W dzisiejszym poście zaprezentuję sposób uzyskania takiego efektu w ASP.NET MVC w którym jak wiemy nie ma żadnych wbudowanych kontrolek.

image 

Dla zwizualizowania, efekt końcowy pokazuję na powyższym screenie. Mamy kilka kolumn, które są linkami. Po kliknięciu na daną kolumnę chcemy aby wyniki były posortowane za pomocą tej kolumny. Po drugim kliknięciu w tą samą kolumnę kierunek sortowania zostaje odwrócony (z DESC na ASC i z ASC na DESC).

Zdecydowałem się na napisanie metody rozszerzającej HtmlHelper:

public static class HtmlHelper
{
   public static string SortableColumn(this System.Web.Mvc.HtmlHelper htmlHelper, string linkText, string columnName, object routeValues)
   {
       return SortableColumn(htmlHelper, linkText, columnName, routeValues, "sord", "sidx",null);
   }
   public static string SortableColumn(this System.Web.Mvc.HtmlHelper htmlHelper, string linkText, string columnName, object routeValues, string sortTypeParName, string sortColumnParName)
   {
       return SortableColumn(htmlHelper, linkText, columnName, routeValues, sortTypeParName, sortColumnParName);
   }
   public static string SortableColumn(this System.Web.Mvc.HtmlHelper htmlHelper, string linkText, string columnName, object routeValues,string sortTypeParName,string sortColumnParName,string anchorName)
   {
       System.Web.Routing.RouteData data = htmlHelper.ViewContext.Controller.ControllerContext.RouteData;
       string actionName = data.GetRequiredString("action");
       
       StringBuilder sb = new StringBuilder();
       var vals = new RouteValueDictionary(routeValues);
       string sidx = String.Empty;
       if (System.Web.HttpContext.Current.Request[sortColumnParName] != null)
       {
           sidx = System.Web.HttpContext.Current.Request[sortColumnParName].ToString();
       }
       //modify the sidx       
       if (vals.ContainsKey(sortColumnParName) == false)
       {
           vals.Add(sortColumnParName, columnName);
       }
       else
       {
           vals[sortColumnParName] = columnName;
       }
       
       string sord = String.Empty;
       if (System.Web.HttpContext.Current.Request[sortTypeParName] != null)
       {
           sord = System.Web.HttpContext.Current.Request[sortTypeParName].ToString();
       }
       //add the sord key if needed      
       if (vals.ContainsKey(sortTypeParName) == false)
       {
           vals.Add(sortTypeParName, String.Empty);
       }
       //if column matches    
       if (sidx.Equals(columnName, StringComparison.CurrentCultureIgnoreCase) == true)
       {
           if (sord.Equals("asc", StringComparison.CurrentCultureIgnoreCase) == true)
           {
               //draw the ascending sort indicator using the wingdings font.  
               sb.Append(" <img src='/images/asc.png' alt='asc'/>");
               vals[sortTypeParName] = "desc";
           }
           else
           {
               sb.Append(" <img src='/images/desc.png' alt='desc'/>");
               vals[sortTypeParName] = "asc";
           }
       }
       else
       {
           vals[sortTypeParName] = "asc";                
       }
       if(anchorName==null)
           sb.Insert(0, System.Web.Mvc.Html.LinkExtensions.ActionLink(htmlHelper, linkText, actionName,vals));
       else
           sb.Insert(0, System.Web.Mvc.Html.LinkExtensions.ActionLink(htmlHelper, linkText, actionName, null, null, null, anchorName, vals, null));

       return sb.ToString();
   }
}

Powyższy kod jest przeróbką przykładu znalezionego na jednym z serwisów (nie pamiętam już niestety nazwy). Metoda przyjmuje kilka parametrów:

  1. object routeValues – tak samo jak w przypadku ActionLink (dodatkowe parametry do przekazania w URL).
  2. string sortTypeParName – nazwa parametru QueryString, który będzie przechowywać typ sortowania.
  3. string sortColumnParName – nazwa parametru QueryString, który będzie przechowywać nazwę kolumny po której należy sortować.
  4. string anchorName – dodatkowy parametr przechowujący nazwę kotwicy (np. #comments). Jeśli przekażemy null metoda zignoruje po prostu kotwicę.

Sposób wykorzystania metody jest zdecydowanie łatwiejszy. W widoku wystarczy:

<table class="tableList">
    <tr>
       
        <th><%=Html.SortableColumn("Obietnica", "ModificationDate", null, "promisesSortType", "promisSortColumnName", "promises")%></th>                 
        <th><%=Html.SortableColumn("Źródło", "Source", null, "promisesSortType", "promisSortColumnName", "promises")%></th>
    </tr>
    ...
</table>

W powyższym przykładzie ostatni parametr (promises) odpowiada za dołączenie kotwicy #promises. SortableColumn wygeneruje odpowiedni link z tekstem i obrazkiem symbolizującym kierunek sortowania (zielona strzałka na pierwszym screenie).

Pozostało jeszcze pokazać zawartość kontrolera, który odpowiada za faktyczne posortowanie danych:

public ActionResult Details(Guid id)
{  
  string promisesOrderString = (Request.QueryString["promisesSortColumnName"] ?? "ModificationDate") + " " + (Request.QueryString["promisesSortType"] ?? "asc");
    
  PromisesRepository promisesRepository = new PromisesRepository();
  IQueryable<Promises> allOrderedPromises= promisesRepository.GetAllOrderedBy()(promisesOrderString);
  
  ...
  return View(viewModel);
}

Kluczem jest wygenerowania string’a w formacie ‘COLUMN_NAME ASC’ lub ‘COLUMN_NAME DESC’. W powyższym kodzie promisesOrderString zawiera właśnie taki  string. Następnie używamy jednej z przeładowanych metod OrderBy i przekazujemy promisesOrderString. Należy zaznaczyć, że domyślnie LINQ nie zawiera metody OrderBy akceptującej jako parametr wejściowy zmienną typu string. Należy więc ją dodać jako rozszerzenie (kod klasy z CodePlex).