niedziela, 2 września 2012

ASP.NET Personalizacja

Jako że od pewnego czasu moja praca zawodowa zmierza bardziej w kierunku ASP postanowiłem nauczyć się czegoś nowego. Tym razem zajmę się personalizacją.

Dodawanie prostej właściwości personalizacji

Powiedzmy że chcemy przechować następujące informacje o użytkowniku:
  • imię
  • nazwisko
  • data ostatniej wizyty
  • wiek
  • status członkostwa
Na początek utwórzmy sobie nowy pusty projekt:

Aby skorzystać z dobrodziejstw personalizacji należy wykonać kilka zmian w pliku Web.config
W sekcji <system.web> należy dodać element <profile> a w nim kolejny element <properties>. To właśnie tam można zdefiniować wszystkie właściwości które będą przechowywane przez silnik personalizacji.
W moim przypadku Web.config wygląda w tej chwili tak:

<configuration>
    <system.web=".web">
      <profile>
        <properties>
          <add name="FirstName" />
          <add name="LastName" />
          <add name="LastVisited" />
          <add name="Age" />
          <add name="Member" />
        </properties>
      </profile>
      <compilation debug="false" targetframework="4.5">
      <httpruntime targetframework="4.5">
    </httpruntime></compilation></system>
</configuration>
Po zdefiniowaniu właściwości personalizacji wykorzystanie ich jest wyjątkowo łatwe.
Utworzymy teraz prosty formularz, który pobiera wcześniej ustalone informacje od użytkowników.
Przykładowy kod pliku aspx może wyglądać tak:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <p>Imię:
                <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
            </p>
            <p>Nazwisko:
                <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox>
            </p>
            <p>Wiek:
                <asp:TextBox ID="TextBox3" runat="server" MaxLength="3"></asp:TextBox>
            </p>
            <p>
                <asp:RadioButtonList ID="RadioButtonList1" runat="server">
                    <asp:ListItem Value="1">Tak</asp:ListItem>
                    <asp:ListItem Value="0" Selected="True">Nie</asp:ListItem>
                </asp:RadioButtonList>
            </p>
            <p>
                <asp:Button ID="Button1" runat="server" Text="Zatwierdź" OnClick="Button1_Click" />
            </p>
            <hr />
            <asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>
        </div>
    </form>
</body>
</html>


Obsługa zdarzenia Button_Click:
    protected void Button1_Click(object sender, EventArgs e)
    {
        if (Page.User.Identity.IsAuthenticated)
        {
            Profile.FirstName = TextBox1.Text;
            Profile.LastName = TextBox2.Text;
            Profile.Age = TextBox3.Text;
            Profile.Member = RadioButtonList1.SelectedItem.Text;
            Profile.LastVisited = DateTime.Now.ToString();

            Label1.Text = "Przechowwane informacje to:" +
                "Imię: " + Profile.FirstName +
                "Nazwisko: " + Profile.LastVisited +
                "Wiek: " + Profile.Age +
                "Członek: " + Profile.Member +
                "Ostatnia wizyta: " + Profile.LastVisited;
        }
        else {
            Label1.Text = "Musisz być pozytywnie zweryfikowany!";
        }
    }

W tym przykładzie zakładamy na razie, że użytkownik przeszedł już proces uwierzytelniania. Jeśli teraz nie dalibyśmy tego if'a, to wystąpiłby wyjątek.
Samym procesem uwierzytelniania zajmę się później.

Fajne jest to że VisualStudio rozpoznaje właściwości wpisane w plik Web.config przy pisaniu metody obsługi kliknięcia przycisku:

Jest to możliwe dzięki temu, że klasa Profile jest ukryta i kompilowana dynamicznie w tle za każdym razem, gdy modyfikowany jest plik Web.config.

Wszystkie elementy przechowywane w systemie personalizacji są rzutowane na określony typ. Domyślnym typem jest String.

Dodawanie grup właściwości personalizacji.

Jeśli potrzebna nam jest większa ilość właściwości personalizacji może zachodzić potrzeba uporządkowania ich w grupy. Jest to możliwe poprzez prostą modyfikację w pliku Web.config. Oto zmodyfikowana część <profile>:

<profile>
      <properties>
        <add name="FirstName"/>
        <add name="LastName"/>
        <add name="LastVisited"/>
        <add name="Age"/>
        <add name="Member"/>

        <group name="MemberDetails">
          <add name="Member"/>
          <add name="DateJoined"/>
          <add name="PaidDuesStatus"/>
          <add name="Location"/>
        </group>

        <group name="FamilyDetails">
          <add name="MarriedStatus"/>
          <add name="DateMarried"/>
          <add name="NumberChildren"/>
          <add name="Location"/>
        </group>
        
      </properties>
</profile>

Jak widać dodane zostały nowe właściwości wewnątrz znaczników <group> które obowiązkowo muszą posiadać atrybut "name". Dzięki temu możwmy się odwoływać do właściwości profilu w następujący sposób:

Profile.MemberDetails.Location
Profile.FamilyDetails.Location

Jak widać mamy teraz 2 atrybuty Location do których możemy się odwołać podając uprzednio nazwę grupy.

Zmiana domyślnego typu przechowywanej wartości

Aby domyślnym typem nie był string, wystarczy do ustalonych już właściwości dodać atrybut "type":

<add name="FirstName" type="System.String"/>
<add name="LastName" type="System.String"/>
<add name="LastVisited" type="System.DateTime"/>
<add name="Age" type="System.Int32"/>
<add name="Member" type="System.Boolean"/>

Podobnie możemy ustawiać wartości domyślne za pomocą atrybutu "defaultValue":

<add name="Member" type="System.Boolean" defaultValue="false"/>


Co jednak zrobić jeśli chcemy przechowywać własny zdefiniwoany przez siebie typ danych?
Stwórzmy sobie klasę reprezentującą wózek z zakupami:


public class ShoppingCart
{
    [Serializable]
 public class ShoppingCart()
 {
  private string PID;
        private string CompanyProductName;
        private int Number;
        private decimal Price;
        private DateTime DateAdded;

        public ShoppingCart(){}

        public string ProductID{
            get{return PID;}
            set {PID = value;}
        }

        public string ProductName
        {
            get{return CompanyProductName;}
            set{CompanyProductName=value;}
        }

        public int NumberSelected{
            get{return Number;}
            set{Number = value;}
        }

        public decimal ItemPrice{
            get{return Price;}
            set{Price = value;}
        }

        public DateTime DateItemAdded
        {
            get{return DateAdded;}
            set{DateAdded=value;}
        }

 }
}

Zauważmy, że klasa posiada atrybut Serializable. Jest to wymagane w wypadku, gdy korzystamy z niej jako wartości personalizacji, która jest zapisywana po stronie serwera w jakimś magazynie danych. Można ją wtedy przenieść np do bazy danych w postaci binarnej lub xml.

Teraz w pliku Web.config możemy dodać np coś takiego:

<add name="Cart" type ="SchoppingCart" serializeAs="Binary"/>

Atrybut SerializeAs może przyjmować następujące wartości:

  • Binary - przechowywanie w postaci binarnej
  • ProviderSpecific - przechowywanie w postaci zdefiniowanej przez dostawcę (silnik personalizacji nie dokonuje serializacji lecz dostawca)
  • String - ustawienie domyślne, przchowywanie w postaci łańcucha znaków
  • XML- serializacja do postaci XML

środa, 18 lipca 2012

ASP.NET MVC

Zgodnie z zaleceniami w komentarzach w jednym z poprzednich postów ruszyłem w stronę MVC.
Opiszę poniżej czego dowiedziałem się wczoraj i postaram się tym samym wprowadzić w MVC tych, którzy nie mieli jeszcze z tym do czynienia.
Ja osobiście z modelem MVC miałem do czynienia już wcześniej gdy pisałem jeszcze w PHP używając Zend Frameworka.

Po pierwszym spotkaniu z MVC w ASP.NET wydaje mi się przyjemniejszy niż w Zendzie.

  • Co to znaczy MVC?
  • To znaczy Model, View, Controller
  • Co oznaczają te słowa?
  • Oznaczają to że całą stronę dzielimy na 3 części. Poruszanie się po stronie nie przypomina już odwoływania się do kolejnych plików i renderowania ich zawartości.
  • Więc jak to działa?
  • Można sobie wyobrazić tak, że wszystkie żądania naszej strony (obojętnie czy wchodzimy na stronę główną czy jakąś tam inną stronę "kontakt", "portfolio" czy jakąkolwiek inną) przechodzą przez zawsze ten sam punkt, który decyduje co robić dalej. (mechanizm trasowania)
  • Po co to?
  • Żeby ułatwić sobie pracę przy utrzymywaniu i rozbudowaniu serwisu. Dzięki stosowaniu kilku zasad i rozdzieleniu strony na moduły można łatwiej nią zarządzać. Wszystko jest poukładane dzięki rozdzieleniu strony na Modele, Widoki i Kontrolery.
  • Co to jest Model?
  • Jest to zestaw klas opisujących dane, na których pracuje aplikacja. Wszystko co związane z dostępem do danych oraz ich modyfikacji (reguły biznesowe).
  • Co to jest Widok?
  • Jest tym co widzi użytkownik. Jest częścią odpowiedzialną tylko i wyłącznie za pokazywanie wyników, przetworzonych danych, które otrzymał do wyświetlenia. No i oczywiście za wszystkie bajery które użytkownik widzi i może wchodzić z nimi w interakcję (interfejs użytkownika).
  • Co to jest Kontroler?
  • Jest to zestaw klas, który ma za zadanie łączenie modelu i widoku. Odpowiada za przepływ działań w aplikacji.

"Teoretyczny przykład"

  • Wchodzę na swoją stronę np jakaśstrona.pl/projekty/asp/10
  • Co "zobaczy" ASP.NET MVC ? Coś takiego:
  • Znajdź kontroler projekty
  • Znajdź metodę asp
  • Prześlij do niej parametr 10 co odpowiada wyświetleniu 10tego (np jakiegoś fikcyjnego) projektu
Kontrolerem jest jakaś klasa, która zawiera metody. Wpisując taki adres jak wyżej, zostaje on rozpoznany przez mechanizm trasowania i znajdowany jest właśnie ten jeden określony kontroler i z niego wywoływana jest metoda (do której przesyłany zostaje parametr będący na samym końcu adresu).

W wyniku wykonania tej metody, jej rezultat przesyłany jest do widoku który wyświetla tylko już gotowe przesłane do niego wyniki. 

Oczywiście wewnątrz metody kontrolera mogą wystąpić odwołania do modelu, który np mógłby pobierać coś z bazy danych. Model możemy wyobrazić sobie jako klasę która reprezentuje jakąś tabelę w bazie danych. Chcąc pobrać coś z bazy wywołujemy jedynie metodę modelu. Cała logika odpowiadająca za połączenie z bazą i zapytania SQL zawiera się właśnie wewnątrz tej klasy/jej metod.

Powinno być już tutaj bardziej widać podział na 3 części.

"Praktyczny przykład"


Nie owijając dalej w bawełnę, pokażę jak "ruszyć" pierwszy projekt MVC.
Od teraz zakładam że każdy wie jak utworzyć projekt i dodawać do niego istniejące lub nowe pliki ;)

Odpalamy VisualStudio,
Tworzymy nowy projekt, o taki:


Ponieważ nie chcemy (jeszcze) robić testów jednostkowych, w następnym oknie zaznaczamy


Po utworzeniu projektu i odpaleniu go (F5) zobaczymy szkielet strony. Klikając w "About" zobaczymy, że pasek adresu w przeglądarce zmienił się na mniej więcej taki: http://localhost:50843/Home/About. 
Tak jak pisałem wcześniej: Wyszukiwany jest kontroler Home i wykonywana jest jego metoda About.

Sprawdźmy czy aby na pewno.

Rzućmy okiem na Solution Explorer.


Mamy foldery Models, Views, Controllers. Chyba nazwy same sugerują co należy tam umieszczać :)
Otwórzmy HomeController. Co widzimy w środku?

public ActionResult About()
{
    return View();
}

No i mamy naszą metodę About.

Kilka uwag co do konwencji

  • Wszystkie nazwy plików wewnątrz folderu Controllers muszą się kończyć na "Controller"
  • W folderze Views każdy kontroler ma swój własny folder z widokami. Nazwy folderów nie zawierają już końcówki "Controller".
  • W tych folderach, pliki .aspx reprezentują widoki. To one są renderowane i wyświetlane po przejściu przez metodę kontrolera.
A co z "Home"? Przecież klikając na Home nie mamy adresu http://localhost:50843/Home/Index tylko http://localhost:50843/.

Racja. Otwórzmy więc plik Global.asax.
Jest to plik w którym definiowane są "ścieżki" naszej aplikacji.
Widzimy w środku funkcję:

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

        }

Widać z grubsza co ona "robi".
Mamy tutaj coś takiego jak MapRoute(...). określona jest tutaj ścieżka, która zawiera w sobie najpierw nazwę kontrolera, później nazwę akcji (metody kontrolera) i parametr "id". Jak na razie nasza strona rozpoznaje tylko taki rodzaj adresu. Własnie tutaj możemy zdefiniować inne ścieżki oraz dodawać wiele innych parametrów. Dalej mamy ustawiony domyślny kontroler akcję oraz parametr, który jest pomijany. Dzięki temu po wejściu na "Home" mamy "czysty" URL.

Wróćmy do pliku HomeController.cs.
Za co odpowiada ViewData["Message"] = "Welcome to ASP.NET MVC!"; ?
Żeby się tego dowiedzieć musimy otworzyć Views/Home/Index.aspx.

Znajdziemy tam linijkę

<h2><%: ViewData["Message"] %></h2>

Widać że odwołuje się do tego samego "elementu".


ViewData jest typem ViewDataDictionary a więc jest słownikiem. Można go używać jak tablicy, do której w kontrolerze w pole "Message" przypisujemy "Welcome to ASP.NET MVC!" aby potem odczytać je w widoku. Zamiast Message możemy wpisać jakiś inny string np "komunikat" czy cokolwiek. Wiemy o co chodzi...



Odnośnie komentarza mgibas w moim wcześniejszym poście, również polecam zainteresowanie się  materiałami Scotta Hanselmana. Sam się właśnie za nie zabieram. Nawet wczoraj w swojej nieszczęsnej książce do ASP.NET w rozdziale właśnie o MVC było o nim wspomniane (nawet jest on jednym z autorów oryginału tej książki), tym bardziej wzbudził moje zanteresowanie.

sobota, 14 lipca 2012

ASP Wiązanie danych cz. 2

W dzisiejszym poście przedstawię kontrolkę LinqDataSource oraz sposób jej implementacji w projekcie Web Site oraz Web Application.

LinqDataSource odwołuje się do bazy danych inaczej niż SqlDataSource. Nasza dzisiejsza kontrolka do pracy potrzebuje obiekt kontekstu. Zanim pokażę jak go utworzyć potrzebujemy znowu jakieś przykładowe dane. Tym razem nasze dane utworzymy sami:

Tworzymy nową aplikację Asp.NET Empty Web Application:


Następnie w Solution Explorerze klikamy prawym przyciskiem na nazwę projektu i wybieramy Add->Add ASP.NET Folder-> App_Data. Na poniższym screenie jest na szaro ponieważ już dodałem go do swojego projektu.

App_Data jest to jeden ze specjalnych folderów dla ASP.NET. W nim będzie przechowywana nasza baza danych, którą teraz utworzymy:
Klikamy w Solution Explorerze na App_Data prawym i dajemy kolejno Add->New Item...
Wybieramy SQL Server Database


Po dodaniu nowej bazy danych będzie ona widoczna w oknie Solution Explorera. Rozwijamy zatem drzewo naszej bazy i klikamy prawym na Tables i wybieramy Add New Table:


Po wybraniu tej opcji ukaże nam się projektant tabeli, gdzie możemy ustalić jej kolumny, typ przechowywanych danych i wiele innych rzeczy. Ja ustawiłem w swoim przypadku taką strukturę:


Pierwsza kolumna oznacza w moim przpadku identyfikator filmu i jest oznaczona jako klucz główny tabeli. Dzięki temu każdy wiersz będzie miał odmienną wartość MovieID, która zresztą będzie się zwiększała automatycznie (przekonamy się o tym przy wprowadzaniu danych).
Myślę że nie trzeba tłumaczyć jak wprowadzać dane do projektanta. Dopiszę tylko, że klucz główny ustawia się po kliknięciu prawym przyciskiem np na nazwie kolumny. Zapiszmy naszą tabelę.

Teraz kliknijmy prawym przyciskiem w Solution Explorerze na dopiero co utworzonej nowej tabeli i wybierzmy opcję Show Table Data. Uzupełnijmy ją jakimiś wartościami. Dla przykładu moje bezsensowne dane wyglądają tak: :)


Po zapisaniu zmian mamy już na czym eksperymentować. Teraz zajmiemy się utworzeniem obiektu kontekstu.
W projekcie Web Application tworzy się go inaczej niż w projekcie Web Site.
Musimy dodać do naszego rozwiązania nowy projekt class library. Klikamy w Solution Explorerze na nasze rozwiązanie (Solution) i dalej: prawy przycisk myszy -> Add -> New Project i wybieramy Class library.


W projekcie usuwamy automatycznie wygenerowany plik .cs i klikając na nowo dodanym projekcie prawym przyciskiem wybieramy Add->New item -> LINQ to SQL Classes


Do nowo utworzonego projektanta przeciągamy teraz naszą tabelę z Server Explorera i... tyle. Obiekt kontekstu został utworzony automatycznie.


Dodawanie kontrolki LinqDataSource

W projekcie naszej aplikacji (a nie w projekcie obiektu kontekstu) dodajmy nowy plik Web Form. Dodajmy także referencję do biblioteki z naszym obiektem kontekstu (pamiętając najpierw o skompilowaniu go).

Otwórzmy teraz nasz plik Web Form z rozszerzeniem .aspx. Z Toolboxa przeciągnijmy do niego LinqDataSource (znajdziemy tą kontrolkę pod zakładką Data w Toolboxie).


Przejdźmy na widok Design lub Split i z menu inteligentnych znaczników wybierzmy Configure Data Source

Jeśli wszystko wykonaliśmy do tej pory poprawnie to w oknie kreatora będziemy mieli widok podobny do tego:

Klikamy Next, w następnym oknie wybieramy naszą tabelę jeśli jeszcze nie została wybrana automatycznie i dajemy Finish. Znaczniki naszej kontrolki zostaną zmodyfikowane automatycznie. Dodatkowo uzyskamy możliwość zaznaczenia opcji ustawienia dodawania, zmiany i usuwania danych z poziomu strony.


Zaznaczmy wszystkie opcje.

Wyświetlanie danych na stronie

Mając wszystko ustawione spróbujmy wyświetlić dane z tabeli na stronie.
Przeciągnijmy na stronę GridView z Toolboxa.
Podpięcie naszego LinqDataSource do GridView jest banalnie proste. Wystarczy tylko w widoku split lub design włączyć menu inteligentnych znaczników i wybrać odpowiednia wartość Choose Data Source: (w moim przypadku LinqDataSource1).


Widzimy że wygląd kontrolki zmienił się. 
Zaznaczmy w naszym GridView podobnie jak w LinqDataSource dostępne checkboxy:


Trzy ostatnie checkboxy pojawią się tylko wtedy gdy zaznaczyliśmy je także wcześniej w LinqDataSource.

Zostało nam tylko uruchomić projekt (F5) i zobaczyć jak wszystko działa.
(Zamiast GridView możemy wybrać inną kontrolkę do prezentowania danych np DetailsView)

Zmiany dla projektu Web Site

W projekcie Web Site wszystko robimy niemal identycznie poza dodawaniem obiektu kontekstu.
Nie tworzymy nowego projektu Class Library i nie dodajemy referencji do naszego głównego projektu tak jak wcześniej. Zamiast tego wszystko odbywa się w jednym projekcie Website.
Musimy dodać do projektu jedynie jeden ze specjalnych folderów ASP.NET App_Code który jest dostępny jedynie dla projektu Web Site.


Do tego folderu dodajemy dopiero LINQ to SQL Classes i robimy wszystko dalej tak samo jak w projekcie Web Application.

Struktura projektu powinna wyglądać mniej więcej tak:


czwartek, 12 lipca 2012

ASP Wiązanie danych cz. 1

Ponieważ ostatnio zajmuję się ASP.NET postanowiłem napisać parę postów odnośnie wiązania danych. Tak więc lecimy:

ASP.NET idzie nam na rękę jeśli chodzi o połączenia z bazami danych. Nie trzeba pisać tak dużo zbędnego kodu jak to miało miejsce wcześniej. W tym poście zaprezentuję kontrolkę SqlDataSource która umożliwia uzyskanie dostępu do dowolnego źródła danych (należącego do grupy dostawców ADO.NET). Domyślnie kontrolka ta posiada możliwość współpracy z dostawcami ODBC, OLE DB, SQL Server, Oracle, SQL Server CE.

Zanim przejdziemy do rzeczy, najpierw pobierzmy jakieś dane które będziemy mogli powiązać. Posłużę się tutaj przykładem z książki z której w dalszym ciągu korzystam przy pisaniu stron ASP.NET i zaproponuję pobranie bazy danych Northwind. Jest to przykładowa baza danych dostępna na stronach Microsoftu do ściągnięcia. Dostępna jest pod adresem http://www.microsoft.com/en-us/download/details.aspx?id=23654


Po ściągnięciu instalatora i domyślnej instalacji powinniśmy na dysku C mieć folder SQL Server 2000 Sample Databases a w nim pliki:


Mamy już wszystko co potrzebne. Odpalamy teraz Visual Studio i tworzymy pusty projekt aplikacji ASP.NET:


Do projektu dodajmy nowy element (new item) Web Form


Otwórzmy go i przeciągnijmy do niego kontrolkę SqlDataSource z Toolboxa. Powinna pojawić sie taka linijka:

<asp:sqldatasource id="SqlDataSource1" runat="server"></asp:sqldatasource>

Tworzenie połączenia z bazą danych jest proste dzięki kreatorowi do którego można się dostać gdy mając ustawiony widok Design albo Split klikniemy na:



Pojawi się nowe okno gdzie wystarczy wskazać bazę danych do której chcemy się połączyć:


Jednak może się tak zdarzyć że nie mamy czego tam wybrać. Możemy zatem utworzyć nowe połączenie klikając na przycisk New Connection...

Zakładam że SQL Server jest zainstalowany na komputerze (w moim przypadku instalowany był razem z VisualStudio). Jeśli tak nie jest należy zainstalować SQL Server.

Okno, które nam się pojawiło w moim przypadku należało uzupełnić tak jak na poniższym screenie:


Klikamy OK i powinniśmy mieć do wyboru bazy danych naszego serwera. Jednak jeśli nie ma tam jeszcze NORTHWIND'a dajmy na razie Cancel i przejdźmy do Server Explorera



Kliknijmy prawym przyciskiem myszy na Data Connections a następnie Add Connection. Okno które wyskoczy uzupełniamy identycznie jak 2 screeny wyżej z tym że w sekcji Connect to a database zaznaczamy Attach a database file:. Następnie klikając na Browse wskazujemy nasz plik NORTHWIND.mdf i zatwierdzamy wszystkie okna. Po rozwinięciu Data Connections w Server Explorerze powinna nam się już pokazać nasza baza danych.

Więc wróćmy do okna "Choose Your Data Connection" i wybierzmy Northwinda:


Klikamy Next i widzimy następujące okno:


Tutaj możemy wybrać tabelę z jakiej chcemy wyświetlać dane lub wywołać niestandardowe polecenie SQL. Jeśli zdecydujemy się na wybranie jednej tabeli możemy zaznaczyć potrzebne nam kolumny lub gwiazdkę (tak jak na powyższym screenie) która oznacza wszystkie kolumny. Dodatkowo mamy jeszcze po prawej stronie przyciski WHERE gdzie możemy ustawiać warunki dla naszego zapytania SQL (np żeby wybierał tylko te wartości z tabeli gdzie w jakiejś kolumnie wartośc jest np większa od 5), ORDER BY gdzie ustalamy kolejność wyświetlania znalezionych elementów oraz Advanced gdzie możemy wybrać opcję Generate INSERT, UPDATE, and DELETE statements która pozwoli wygenerować podane instrukcje na podstawie utworzonej instrukcji SELECT.
Kolejna opcja Use optimistic concurrency sprawia że po zaznaczeniu jej, instrukcje UPDATE i DELETE będą tak modyfikowane aby zawierały wartości oryginalne jak i nowe. Dzięki temu, w momencie wykonywania zapytań, dane docelowe będą porównywane z danymi w kontrolce SqlDataSource. Jeśli rekord zmienił się od czasu pobrania go przez kontrolkę to instrukcje UPDATE i DELETE nie zostaną wykonane.

Wybierzmy kilka kolumn i naciśnijmy Next


Możemy sprawdzić wynik wygenerowanego zapytania w następnym oknie przy pomocy przycisku Test Query.


Po kliknięciu na Finish widzimy że nasza kontrolka została nieco zmodyfikowana.

<asp:SqlDataSource ID="SqlDataSource1" runat="server"
            ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
            SelectCommand="SELECT [CustomerID], [ContactName], [CompanyName], [Country] FROM [Customers]"></asp:SqlDataSource>

Na tym zakończę część pierwszą.
Więcej w części drugiej już wkrótce.

czwartek, 24 maja 2012

Program wielowątkowy

Napiszemy prosty program wielowątkowy, zrobimy synchronizację z głównym wątkiem (aby czekał dokładnie do momentu zakończenia drugiego wątku) i dodatkowo prześlemy parametr do nowo tworzonego wątku. To wszystko jest często niezbędne w aplikacjach i warto wiedzieć jak to się robi.

Poprzednio wątki synchronizowane były za pomocą zmiennej bool. Nie jest to dobre rozwiązanie. Dodatkowo wątek główny musiał sprawdzać co pewien czas wartość tej zmiennej. Tutaj rozwiążemy to nieco bardziej optymalnie.

Stwórzmy w nowym konsolowym projekcie nową klasę:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Threads1
{
    class AddParams
    {
        public int a, b;

        public AddParams(int num1, int num2)
        {
            a = num1;
            b = num2;
        }
    }
}

Zwykła klasa posiadająca publiczne pola a i b oraz konstruktor.
Narazie nie wnikamy po co ona jest.
Teraz napiszmy w głownej klasie programu nową metodę statyczną Add:
static void Add(object data)
{
    Console.WriteLine("ID wątku metody Add(): {0}",
        Thread.CurrentThread.ManagedThreadId);

    AddParams ap = (AddParams)data;
    Console.WriteLine("{0} + {1} = {2}",
        ap.a, ap.b, ap.a + ap.b);
}

Ta metoda będzie punktem wejścia wątku, czyli miejscem z którego wątek zacznie swoją pracę. Treść funkcji nie jest skomplikowana. Przyjmuje ona jeden parametr typu object. Ponieważ jest to zwykły obiekt, możemy przesłać do tej metody wsystko. Tak też zrobimy (po to właśnie została utworzona wcześniej klasa AddParams).

Czyli podsumowując:

  • Mamy stworzoną metodę od której nowy wątek zacznie działanie
  • Metoda przyjmuje parametr object który następnie rzutowany jest na wcześniej stworzoną klasę, z której wyciągane są parametry i wykonywane jest dodawanie.

Tworzenie nowego wątku

Dodajmy jeszcze minimalne uśpienie wątku (dla testu). Nie jest ono potrzebne, lecz pomoże zrozumieć istotę synchronizacji.

static void Add(object data)
{
    Console.WriteLine("ID wątku metody Add(): {0}",
        Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(100);
    AddParams ap = (AddParams)data;
    Console.WriteLine("{0} + {1} = {2}",
        ap.a, ap.b, ap.a + ap.b);
}


Utwórzmy teraz nowy wątek. Metoda Main() aplikacji wygląda następująco:

static void Main(string[] args)
{
    AddParams ap = new AddParams(10, 10);
    Thread t = new Thread(new ParameterizedThreadStart(Add));
    t.Start(ap);
    Console.WriteLine("Finished");
    Console.ReadLine();
}

Nowy wątek to obiekt typu Thread. W konstruktorze tego obiektu  podajemy nowy obiekt ParametrizedThreadStart do którego konstruktora podajemy naszą metodę Add.
Dzięki temu że jest to obiekt ParametrizedThreadStart możliwe jest w następnej linii podanie obiektu z parametrami: t.Start(ap).

Tutaj uwidoczni się teraz problem synchronizacji. Gdyby nie Console.ReadLine w głownej metodzie Main() to program zamknął by się natychmiast. Tutaj pokazane przykładowe działanie:

Jak widać wynik metody Add został obliczony dopiero po tym jak główny wątek zakończyłby już program. Zasymulowaliśmy to za pomocą Thread.Sleep(100);

Synchronizacja wątków

Aby wątek główny czekał na wątek poboczny do chwili jego zakończenia zdefiniujmy statyczny obiekt AutoResetEvent przed metodą Main():

private static AutoResetEvent waitHandle = new AutoResetEvent(false);

Przy konstruktorze podajemy wartość false. Oznacza to że nie przesłano jeszcze powiadomienia o zakończenia wątku pobocznego.
Teraz jeśli chcemy aby wątek główny rozpoczął oczekiwanie na wątek poboczny dopiszmy:

waitHandle.WaitOne();

Teraz w funkcji Add(..) aby poinformować o zakończeniu pracy wystarczy dopisać:

waitHandle.Set();

A efekt działania programu po tej zmianie:

Jak widać wątek główny zakończył się natychmiast po zakończeniu wątku pobocznego.
A teraz cały kod dla jasności:
Program.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Threads1
{
    class Program
    {
        private static AutoResetEvent waitHandle = new AutoResetEvent(false);

        static void Main(string[] args)
        {
            AddParams ap = new AddParams(10, 10);
            Thread t = new Thread(new ParameterizedThreadStart(Add));
            t.Start(ap);
            waitHandle.WaitOne();
            Console.WriteLine("Finished");
            Console.ReadLine();
        }
        static void Add(object data)
        {
            Console.WriteLine("ID wątku metody Add(): {0}",
                Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(100);
            AddParams ap = (AddParams)data;
            Console.WriteLine("{0} + {1} = {2}",
                ap.a, ap.b, ap.a + ap.b);
            waitHandle.Set();
        }
    }
}


AddParams.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Threads1
{
    class AddParams
    {
        public int a, b;

        public AddParams(int num1, int num2)
        {
            a = num1;
            b = num2;
        }
    }
}

środa, 23 maja 2012

Szybkie dodawanie przestrzeni nazw

Jeśli znamy obiekt z którego chcemy skorzystać a nie pamiętamy w jakiej przestrzeni nazw się znajduje (nie wiemy co dopisać po "using") z pomocą przychodzi VisualStudio.

Załóżmy że chcemy użyć wielowątkowości w naszym programie. Potrzebujemy dostać się do klasy Thread. Jednak w jakiej przestrzeni nazw ona się znajduje?


Aby automatycznie dodać odpowiednią przestrzeń nazw wystarczy kliknąć na "Thread" prawym przyciskiem myszy i wybrać Resolve->using System.Threading;

Odpowiednia przestrzeń nazw zostanie dodana automatycznie (jednak musimy mieć w projekcie odpowiednią referencję do biblioteki).

Asynchroniczne wywołanie delegatów

Pora na wielowątkowość!
W najbliższych postach zajmę się wielowątkowością w C#. Jednak zanim przejdę do "prawdziwych" watków (z przestrzeni System.Threading), najpierw asynchroniczne delegaty.

Asynchroniczne delegaty

Delegata można utożsamiać ze wskaźnikiem na funkcję z zachowaniem bezpieczeństwa typów. Takiego delegata można wywołać synchronicznie i asynchronicznie. Asynchroniczne wywołanie delegata daje takie same rezultaty jak utworzenie nowego wątku a jest nawet prostrze! 
Jak się okaże, wystarczy przypisać delegatowi pewną funkcję i zamiast wywołać delegata jak zwykła funkcję użyta zostanie metoda BeginInvoke().

Rzućmy okiem na poniższy programik:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace AsyncDelegate
{
    public delegate int BinaryDelegate(int x, int y);
    class Program
    {
        private static bool isDone = false;

        static void Main(string[] args)
        {
            Console.WriteLine("**** Asynchroniczne delegaty ****");
            Console.WriteLine("Main() wywołana na wątku {0}.",
                Thread.CurrentThread.ManagedThreadId);

            BinaryDelegate b = new BinaryDelegate(Add);
            IAsyncResult theAsRes = b.BeginInvoke(10, 10,
                new AsyncCallback(AddComplete), "Dzięki za współpracę!");

            while (!isDone)
            {
                Thread.Sleep(1000);
                Console.WriteLine("WORKING...");
            }
            //Console.WriteLine("WAITING");
            Console.ReadLine();
        }

        static int Add(int x, int y)
        {
            Console.WriteLine("Add() wywołana na wątku {0}.",
                Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(5000);
            return x + y;
        }

        static void AddComplete(IAsyncResult theAsRes)
        {
            Console.WriteLine("AddComplete() wywołane na wątku {0}.",
                Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("Dodawanie skończone");
            Console.WriteLine((string)theAsRes.AsyncState);
            isDone = true;
        }
    }
}

Teraz po kolei wszystko sobie wyjaśnimy.

  • Jak widać delegat nie musi znajdować się wewnątrz klasy, może być poza nią.
  • Do podejrzenia identyfikatora wątku używamy Thread.CurrentThread.ManagedThreadId
  • Metoda BeginInvoke zawsze zwraca obiekt implementujący interfejs IAsyncResult. Ten obiekt to pewnego rodzaju sprzężenie pozwalające wątkowi wywołującemu (czyli temu głównemu) uzyskać wynik asynchronicznego wywołania metody w późniejszym czasie (np przy pomocy EndInvoke(), którą tutaj pominąłem).
  • Do synchronizacji użyłem tutaj zmienną typu bool. Nie jest to dobra praktyka lecz dla tego przykładu w zupełności wystarczająca. (Nie jest dobra dlatego że każdy z wątków ma do niej jednoczesny dostęp... ale o tym później)
  • Pętla co sekundę sprawdza czy dodawanie już się zakończyło. Zapobiega ona zakończeniu programu zanim wątek poboczny obliczy wynik.

Jednak pozostało jeszcze do rozstrzygnięcia coś dziwnego.
Normalnie wywołując delegata (czy to tak jak funkcję czy za pomocą invoke) podawalibyśmy 2 parametry (agrumenty funkcji "połaczonych" z delegatem). Jednak dla BeginInvoke potrzeba 4 parametrów.

  • Za pomocą pierwszego parametru podaje się funkcję, która zostanie wywołana po zakończeniu wątku pobocznego.
  • Drugi parametr jest typu Object. Pozwala przesłać dowolny obiekt do wybranej wcześniej funkcji.
I tak:
Tworząc nowego delegata AssyncCallback i podając mu jako parametr nazwę funkcji AddComplete (która przyjmuje obiekt IAsyncResult) mamy pewność że funkcja zostanie wywołana natychmiast po zakończeniu wątku pobocznego. Dodatkowo dzięki czwartemu parametrowi, przy pomocy IAsyncResult theAsRes możemy "wyciągnąć" dodatkowy argument przesłany do funkcji przez rzutowanie jego właściwości AsyncState na string.

czwartek, 17 maja 2012

MouseLeftButtonDown na DataGrid

Ostatnio spotkałem się z pewnym problemem związanym z DataGridem. Potrzebowałem oprogramować na nim zdarzenie MouseLeftButtonDown. Jednak nie jest to takie proste. Do pokazania problemu i rozwiązania użyję prostego projektu Silverlight Application.

Tworzenie projektu Silverlight

Utwórzmy zwykły najprostrzy projekt Silverlight Application w Visual Studio.
Za pomocą toolboxa lub bezpośrednio w XAML dodajmy do niego obiekt DataGrid.
Utwórzmy dodatkowo nową klasę Person, której obiektami wypełnimy automatycznie DataGrida. Tutaj przykład klasy:

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

Następnie zaraz po InitializeComponent() w metodzie MainPage() zapiszmy 100 losowych osób do naszego DataGrida.
        List<person> persons = new List<person>();
        for(int i=0;i<100;++i){
            persons.Add(new Person{ FirstName = "Name " + i, LastName = "LastName " + i});
        }
        dataGrid1.AutoGenerateColumns = true;
        dataGrid1.ItemsSource = persons;

Po uruchomieniu projektu powinniśmy zobaczyć wynik podobny do tego:

Problem

Aby przekonać się o problemie MouseLeftButtonDown spróbujmy dopisać reagowanie na to właśnie zdarzenie poprzez wyświetlenie MessageBoxa. Tutaj przykładowa metoda:
private void dataGrid1_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    MessageBox.Show("Mouse Pressed!");
}

Zauważymy że tylko po kliknięciu na górną belkę z nazwami kolumn lub na jedno-pixelową obwódkę wokół naszej kontrolki następuje wyświetlenie MessageBoxa:

A co jeśli np tak jak ja, potrzebujemy koniecznie wyzwolić to zdarzenie po kliknięciu na dany rekord wewnątrz kontrolki? Jak zrobić żeby nasz event nie został "zjedzony" przez wiersze tabeli?

Rozwiązanie problemu

Utworzymy własną kontrolkę dziedziczącą po  DataGrid.
public class MyDataGrid : DataGrid
{
    public MyDataGrid()
    {
        this.AddHandler(FrameworkElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(MouseDown), true);
    }
    void MouseDown(object sender, MouseButtonEventArgs e)
    {
        MessageBox.Show("Mouse Left Button Down!");
    }
}

Konstruktor naszego nowego obiektu wywołuje metodę AddHandler, która doda odpowiedni Event do obsłużenia. Dodatkowo jako parametr musimy podać nowy obiekt MouseButtonEventHandler który inicjujemy podając mu nazwę metody wywoływanej przy wciśnięciu lewego przycisku myszki.

Z poprzedniego pliku MainPage.xaml.cs usuńmy wcześniej dopisaną metodę dataGrid1_MouseLeftButtonDown() pamiętając o usunięciu jej także z pliku xaml.
Dodatkowo w pliku xaml zdefiniujmy naszą przestrzeń nazw, w tym przypadku:
xmlns:my="clr-namespace:SilverlightApplication1"

Możemy teraz zmienić znacznik <sdk:datagrid> na <my:mydatagrid> pamiętając o zmianie parametru Name na x:Name.
Jeśli wszystko wykonaliśmy dobrze to po kliknięciu na dane w naszym nowym DataGridzie uzyskamy obsługę zdarzenia kliknięcia lewego przycisku myszki:
Poniżej całkowity kod MainPage.xaml.cs:
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverlightApplication1
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
            List persons = new List();
            for(int i=0;i<100;++i){
                persons.Add(new Person{ FirstName = "Name " + i, LastName = "LastName " + i});
            }
            dataGrid1.AutoGenerateColumns = true;
            dataGrid1.ItemsSource = persons;
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}


MainPage.xaml:


    
        
    



MyDataGrid.cs:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverlightApplication1
{
    public class MyDataGrid : DataGrid
    {
        public MyDataGrid()
        {
            this.AddHandler(FrameworkElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(MouseDown), true);
        }
        void MouseDown(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("Mouse Left Button Down!");
        }
    }
}



poniedziałek, 7 maja 2012

Otwieranie pliku w trybie binarnym Visual Studio

Czasami jeśli nie znamy struktury jakiegoś pliku a chcemy wyciągnąć z niego pewne informacje, przydaje się podejrzenie jego zawartości binarnej. Są do tego celu stworzone specjalne edytory chociażby WinHex.
Jednak jeśli nie zależy nam na głębszej analizie i różnego typu pomocniczych opcjach a chcemy jedynie szybko podejrzeć zawartość pliku to Visual Studio wystarcza zupełnie.

Z otwieraniem plików w trybie binarnym w VS wiąże się pewien "myk".

Robimy kolejno File-> Open-> File:



Wybieramy plik i zamiast Enter czy kliknięcia przycisku Open klikamy strzałkę na tym przycisku:


Następnie Open With...


I wybieramy Binary Editor


Poniżej widzimy otwarty w trybie binarnym plik .tif który zawiera w sobie wielowymiarowe obrazy komórki (obrazy dwuwymiarowe robione dla kolejnych głębokości i w kolejnych odstępach czasu (4D) ). Widać tutaj ładnie takie parametry jak ilość przekrojów (głębokości) - slices oraz ilość klatek na jedną głębokość (16) a także ilość wszystkich obrazków (images):


Zapis danych do pliku binarnego

Dzisiaj dowiemy się jak zapisać i odczytać dane z pliku binarnego - prosta przydatna rzecz

Binary writer

W nowym konsolowym projekcie napiszmy następującą metodę main:

static void Main(string[] args)
    {
        Console.WriteLine("Test zapisu binarnego");
        FileInfo f = new FileInfo("Binfile.bin");
        using (BinaryWriter bw = new BinaryWriter(f.OpenWrite()))
        {
            double zm1 = 12345.678;
            int zm2 = 32123;
            string zm3 = "Test-string";

            bw.Write(zm1);
            bw.Write(zm2);
            bw.Write(zm3);
        }
        Console.ReadLine();
    }

Ponieważ używamy tutaj obiektu FileInfo należy pamiętać o dopisaniu using System.IO;


Kod nie wymaga chyba większego komentarza. Tworzone są 3 zmienne a następnie zapisywane do pliku Binfile.bin.


Binary reader

Jeśli otworzylibyśmy teraz plik Binfile.dat w notatniku to (poza zmienną string) nie wiele byśmy zobaczyli:


Nie tylko notatnik a nawet bezpośredni podgląd pliku binarnego też nie wiele pokazuje:



Spróbujmy zatem odczytać z powrotem te dane za pomocą binary readera.
Do naszej metody main powyżej Console.ReadLine(); dopiszmy:

using (BinaryReader br = new BinaryReader(f.OpenRead()))
        {
            Console.WriteLine(br.ReadDouble());
            Console.WriteLine(br.ReadInt32());
            Console.WriteLine(br.ReadString());
        }

A wynik:


Jak widać wszystko zgodnie z planem.