JQuery

Po miesięcznej pracy nad Androidem w Eclipsie wróciłem do Visual Studio i ASP.NET. Po kilku godzinach pracy stwierdziłem ,że chyba zapomniałem jak się programuje. Do rozwiązania miałem prosty problem ,a ja uznałem ,że do jego rozwiązania jest potrzebny “UpdatePanel”. O to moja historia.

 

Kontrolka UpdatePanel odmówiła mi posłuszeństwa. Zawartość strony aktualizowała się asynchronicznie tylko raz. Kiedyś w przeszłość miałem taki sam problem ,ale tym razem nie udało mi się  ustalić przyczyny.

Potem pomyślałem sobie po co mi UpdatePanel do zapamiętania nowych stanów checkboxów, gdy mogę zrobić to samo w jQuery.

Co mi się udało tyle ,że potem stwierdziłem ,że do rozwiązania problemu nie potrzebuje nawet jQuery. Poczułem się trochę głupio. Ta przerwa od Visual Studio była chyba za długa, bo nawet próbowałem używać skrótów klawiszowy Eclipse.

Dlaczego o tym piszę?

Uznałem ,że niepotrzebne rozwiązanie w jQuery wciąż jest ciekawe i być może kiedyś w innym kontekście mi się ono przyda. Poza tym też jestem człowiek i mogę popełniać błędy.

Czasem  myślę ,że pewne nawyki klasycznego ASP.NET uczą programistów zły nawyków. Coś się zmienia stan dynamicznie wielu elementów na raz potrzebujemy UpdatePanel-a ,aby te zmiany zachować bez pełnego przeładowania strony.

Jeśli chodzi Jquery klasyczny ASP.NET nie lubi gdy mu się modyfikuje stany kontrolek bez jego wiedzy co zwykle wywołuje wyjątek  z powodu mechanizmu ViewState.

W tym wypadku nie jest to jednak potrzebne wystarczyło tylko trochę pomyśleć.

Jaki dokładnie jest problem. Wyobraźcie sobie listę checkboxów, które określają stan danego elementu. Te checkboxy nie zaznaczają elementów do kasowania (taki problem jest dużo łatwiejszy do rozwiązania).

Po zmianie użytkownika muszę określić które elementy uległy zmianie i je zaktualizować.

W ASP.NET nie ma czegoś takiego jak binding (Silverlight,WPF, WinRT). Czyli kolekcja nie zaktualizuje mi się automatycznie wraz ze zmianą użytkownika. To jest coś co ja muszę zrobić.

Zawartość kontrolki “ListView” jest czyszczona w trakcie ponownego wywoływania strony. Ten problem jednak łatwo rozwiązać używając sesji lub Viewstate.

Oto przykład aplikacji ilustrujący moją historię i problem.

Przygotowywanie aplikacji

Aplikacja będzie potrzebować kolekcji elementów.

Postanowiłem się trochę wysilić i stworzyłem tabelkę z danymi. Tabelka App zawiera trzy kolumnę określające ID,Nazwę i stan zablokowania danego elementu.

image

Stworzyłem także procedurę SQL, która wyciągnie te dane z bazy.

CREATE PROCEDURE [dbo].[uspGetApps]
AS
SELECT AppId,Name,IsBlocked FROM Apps

Później tą procedurę wywołuje w C# przy pomocy klasycznych metod ADO.NET.

public static class SQLProvider
{
    private const string ProcedureGetapps = "[dbo].[uspGetApps]";

    public static List<ModelApp> GetListOfApps()
    {
        var list = new List<ModelApp>();

        var connection = new SqlConnection(ConnectionString);
        connection.Open();
        try
        {
            var command = new SqlCommand();
            command.Connection = connection;
            command.CommandType = CommandType.StoredProcedure;
            command.CommandText = ProcedureGetapps;


            var reader = command.ExecuteReader();

            while (reader.Read())
            {
                list.Add(new ModelApp(reader));
            }

            return list;
        }
        catch (Exception)
        {
            throw;
        }
        finally
        {
            connection.Close();
        }
    }

    public static readonly string ConnectionString = 
        ConfigurationManager.ConnectionStrings
        ["ConnectionStringAppDataBase"].ConnectionString;

}

ConnectionString jest zapisany w pliku konfiguracyjnym.

<configuration>
    <connectionStrings>
        <add name="ConnectionStringAppDataBase" 
             connectionString="Data Source=(LocalDB)\v11.0;
             AttachDbFilename=|DataDirectory|\DataBaseApps.mdf;Integrated Security=True"
            providerName="System.Data.SqlClient" />
    </connectionStrings>
    <system.web>
      <compilation debug="true" targetFramework="4.5" />
      <httpRuntime targetFramework="4.5" />
    </system.web>
</configuration>

Oto klasa, która będzie reprezentować obiektową informacje na temat rekordu z tabelki App.

[Serializable]
public class ModelApp
{
    public string Name { get; set; }
    public bool IsChecked { get; set; }
    public int AppId { get; set; }

    public ModelApp(int appId, string name, bool ischecked)
    {
        AppId = appId;
        Name = name;
        IsChecked = ischecked;
    }

    public ModelApp(int appId, bool ischecked)
    {
        AppId = appId;
        IsChecked = ischecked;
    }

    public ModelApp(IDataReader reader)
    {
        AppId = reader.GetInt32(0);
        Name = reader.GetString(1);
        IsChecked = reader.GetBoolean(2);
    }

    public bool PreviousValue { get; set; }

    public bool HasChanged
    {
        get { return PreviousValue != IsChecked; }
    }

    public override string ToString()
    {
        return string.Format("ID: {0} NewState: {1} <br />", AppId, IsChecked);
    }
}

Klasa ta ma 3 konstruktor. Jeden stworzy obiekt na podstawie dany z bazy danych , drugi stworzy obiekt na podstawie danych, jakich podamy ,a trzeci stworzy obiekt “App” bez określenia jego nazwy. Ten ostatni konstruktor przyda się przy rozwiązaniu jQuery.

Klasa ta też zawiera właściwość “PreviousValue”, która będzie określać poprzednią wartość danego elementu.

Kolekcja jest wyciągana z bazy i wiązana z kontrolką “ListView” w zdarzeniu Page_Load dla strony.

protected void Page_Load(object sender, EventArgs e)
{
    if (IsPostBack == false)
    {
        ListBlockedApps = SQLProvider.GetListOfApps();
        lvApps.DataSource = ListBlockedApps;
        lvApps.DataBind();
    }
}

Kolekcję po pobraniu  zapisuje do Viewstate z używając takie właściwości. Teraz mam dostęp do kolekcji po każdym przeładowaniu strony.

public List<ModelApp> ListBlockedApps
{
    get
    {
        if (ViewState["BlockedApps"] != null)
            return (List<ModelApp>)ViewState["BlockedApps"];
        return new List<ModelApp>();
    }
    set
    {
        ViewState["BlockedApps"] = value;
    }
}

Kod strony jest następujący.

<input id="btn_unselectAll" type="button" value="Odznacz wszystkie" />
<div id="divApp" class="divgone">
    <div style="font-size: 12px;">
        Czy można uruchamiać daną aplikacę?
    </div>
    <div style="overflow: scroll; height: 235px; width: 380px; -ms-overflow-x: hidden; overflow-x: hidden;">
        <asp:ListView ID="lvApps" runat="server">
            <LayoutTemplate>
                <asp:PlaceHolder ID="itemPlaceHolder" runat="server" />
            </LayoutTemplate>
            <ItemTemplate>
                <div style="margin-top: 20px; margin-left: 10px; width: 320px; height: 15px;">
                    <div style="float: left; width: 290px;">
                        <asp:Label ID="Label1" Text='<%# Eval("Name") %>' runat="server" />
                    </div>
                    <div style="float: right; width: 30px;" class="_neededdiv">
                        <asp:CheckBox ID="chk_blockedApps" Checked='<%# Eval("IsChecked") %>' 
                            runat="server" />
                        <p style="display: none;" >
                            <asp:Literal runat="server" ID="lblAppId" Text='<%# Eval("AppId") %>' />
                        </p>
                    </div>
                </div>
            </ItemTemplate>
        </asp:ListView>
    </div>
</div>
<br />
<asp:Button ID="btnNormalSave" ClientIDMode="Static" runat="server"
     Text="Zapisz" OnClick="btnSave_Click" />
<asp:Button ID="btnJquerySave" ClientIDMode="Static" runat="server" 
      Text="ZapiszJquery" OnClick="btnSaveJquery_Click" />
<br />
<asp:Literal  ID="lblChanged" runat="server" />
<input id="hiddenField_info" type="hidden" runat="server" />

Obecnie strona wygląda tak.

image

Strona zawiera przycisk, który odznaczy wszystkie checkbox-y przy pomocy magii jQuery.

$('#btn_unselectAll').click(function() {
    $('div._neededdiv').each(function (index) {

        var $this = $(this);

        var $chk = $this.find('input');
        if ($chk.is(':checked')) {
            $chk.attr('checked', false);
        }
    });
});

Każdy przykład jQuery w tym wpisie zapewne wymaga refaktoryzacji ,ale przynajmniej są one czytelne.

Przyciski poniżej zapiszą zmiany. Jeden z nich zapisze zmiany tak jak powinnyśmy to zrobić , drugi natomiast przekaże zmiany kontrolek do ASP.NET przy pomocy jQuery.

Proste rozwiązanie

Proste rozwiązane nie wymaga żadnej magii. Faktycznie po kliknięciu przycisku tracimy informację o kolekcji ,ale używając metody “FindControl” w praktyce możemy znaleźć stan danej kontrolki.

Dla pewność także sprawdziłem czy manipulacja jQuery stanu kontrolek czegoś nie psuje. Dlatego dałem możliwość odznaczenia wszystkich checkboxów po stronie klienta.

Z doświadczenia wiem ,że czasem mechanizm Viewstate bzikuje, gdy się modyfikuje stany kontrolek kodem klienckim. Tym razem jednak problemu nie ma co znaczy ,że rozwiązanie związane z jQuery jest jeszcze bardziej bezużyteczne.

protected void btnSave_Click(object sender, EventArgs e)
{
    var eb = new List<ModelApp>();

    foreach (var item in lvApps.Items)
    {
        var state = ((CheckBox)item.FindControl("chk_blockedApps")).Checked;
        var id = int.Parse(((Literal)item.FindControl("lblAppId")).Text);
        eb.Add(new ModelApp(id, state));
    }

    foreach (var blockedAppEnity in ListBlockedApps)
    {
        foreach (var appEnity in eb)
        {
            if (appEnity.AppId == blockedAppEnity.AppId)
                appEnity.PreviousValue = blockedAppEnity.IsChecked;
        }
    }

    var list = eb.Where(ee => ee.HasChanged);

    lblChanged.Text = "";
    foreach (var modelApp in list)
    {
        lblChanged.Text += modelApp.ToString();
    }
}

Najważniejsza część kodu znajduje się tutaj. W pętli skanuje wszystkie graficzne elementy kontrolki ListView ,a potem używaj metody “FindControl” mogę odnaleźć wartości kontrolek.

Ten kod znany jest mi od moich początków z ASP.NET ,ale z jakiegoś powodu tego dnia w ogóle zapomniałem ,że mogę coś takiego zrobić.  To jest najprostsze i najbardziej prawidłowe rozwiązanie tego problemu.

foreach (var item in lvApps.Items)
{
    var state = ((CheckBox)item.FindControl("chk_blockedApps")).Checked;
    var id = int.Parse(((Literal)item.FindControl("lblAppId")).Text);
    eb.Add(new ModelApp(id, state));
}

Później porównuje nową kolekcję ze starą zapisaną w ViewState.

foreach (var blockedAppEnity in ListBlockedApps)
{
    foreach (var appEnity in eb)
    {
        if (appEnity.AppId == blockedAppEnity.AppId)
            appEnity.PreviousValue = blockedAppEnity.IsChecked;
    }
}

var list = eb.Where(ee => ee.HasChanged);

lblChanged.Text = "";
foreach (var modelApp in list)
{
    lblChanged.Text += modelApp.ToString();
}

Dla ułatwienia przykładu nie aktualizuje kolekcji o wprowadzane zmiany ,ale wyświetlam je na stronie.

image

Rozwiązanie z jQuery

Hyper vRozwiązanie z jQuery bazuje na technice, która jest mi dobrze znana i opisałem ją  już w jednym wpisie

W jQuery zbieram wartość checkbox oraz zapisane id.

 

 

$('#btnSave').click(function () {

    var savethis = '';
    $('div._neededspan').each(function (index) {

        var $this = $(this);

        var $chk = $this.find('input');

        var $id = $this.find('p');

        if ($chk.is(':checked')) {
            console.log(index + ": " + 'true ' + $id.text());
            savethis = $id.text() + '|' + 'true' + ',' + savethis;

        } else {
            console.log(index + ": " + 'false ' + $id.text());
            savethis = $id.text() + '|' + 'false' + ',' + savethis;
        }

    });
    console.log(savethis);
    $('#<%=hiddenField_info.ClientID %>').val(savethis);

    return true;
});

Później cały tekst, który jest na tym obrazu wyświetlony jako ostatni jest zapisany do ukrytego pola “hiddenField_info”.

image

Po stronie C# te ukryte pole odczytuje i używając metody “Split” tworze kolekcje obiektów ModelApp.

protected void btnSave_Click(object sender, EventArgs e)
{
    string f = hiddenField_info.Value;

    string[] tab = f.Split(',');

    var eb = new List<ModelApp>();

    foreach (string s in tab)
    {
        if (!string.IsNullOrWhiteSpace(s))
        {
            string[] tab2 = s.Split('|');

            var fa = int.Parse(tab2[0]);
            var ff = bool.Parse(tab2[1]);
            eb.Add(new ModelApp(fa, ff));
        }
    }

    foreach (var blockedAppEnity in ListBlockedApps)
    {
        foreach (var appEnity in eb)
        {
            if (appEnity.AppId == blockedAppEnity.AppId)
                appEnity.PreviousValue = blockedAppEnity.IsChecked;
        }
    }

    var list = eb.Where(ee => ee.HasChanged);
            
    lblChanged.Text = "";
    foreach (var modelApp in list)
    {
        lblChanged.Text += modelApp.ToString();
    }
}

Większość kodu powiela pierwsze rozwiązanie. Najważniejsza część kodu tworzą kolekcja z wartość pola “hiddeFiled_info” znajduje się tutaj.

pstring f = hiddenField_info.Value;

string[] tab = f.Split(',');

var eb = new List<ModelApp>();

foreach (string s in tab)
{
    if (!string.IsNullOrWhiteSpace(s))
    {
        string[] tab2 = s.Split('|');

        var fa = int.Parse(tab2[0]);
        var ff = bool.Parse(tab2[1]);
        eb.Add(new ModelApp(fa, ff));
    }
}

image

Jako ciekawostkę dodam ,że całe te wyrażenie foreach może być przerobione na wyrażenie LINQ. Jest ono mniej czytelne więc raczej czegoś takiego bym nie umieścił bym w kodzie.

pvar eb = (from s in tab
          where !string.IsNullOrWhiteSpace(s) 
          select s.Split('|') into tab2
          let fa = int.Parse(tab2[0])
          let ff = bool.Parse(tab2[1])
          select new ModelApp(fa, ff)).ToList();

Czego się więc nauczyłem?

Pobierz Kod