piątek, 1 marca 2013

Pinterest / Blocksit layout

Jednym z najnowszych "krzyków mody", jest wyświetlanie newsów na stronie internetowej w "pseudo-tabelaryczny" sposób. Jest to sposób, który na pierwszy rzut oka przypomina nieco tabelę, ale nią nie jest, czyli dane ustawione są w kolumny, ale wysokość wierszy jest różna i zależna od ilości contentu (wysokość zdjęcia, ilość i długość komentarzy do zdjęcia itp.). Dodatkowo, resizując stronę (zmniejszając lub zwiększając okno przeglądarki) newsy się przestawiają w swojej "pseudo tabeli". Jako pierwsze (ale mogę się mylić), taki layout graficzny zastosowało na swojej stronie https://pinterest.com/, dlatego nazwałem ten layout "pinterest layout".

Teoretycznie, można by było zalogować się na stronę https://pinterest.com/ i spróbować przekopiować ich layout, ale takie rozwiązanie nie było by ani zbyt grzeczne, ani zbyt moralnie poprawne. Na szczęście dzięki www.google.com udało mi się znaleźć stronę, która zagregowała 'know how", czyli link do wtfdiary.com, na którym jest 6 'know how to do pinterest loyout'.

Mi osobiście najbardziej przypadł do gustu BlocksIt.js dlatego w dalszej części skupię się właśnie na tym rozwiązaniu. Jego zalety:
- to łatwość i prostota obsługi,
- ładny wygląd zewnętrzny,
- przystępny tutorial (zawierający 2 projekty demo)
- licencja  GNU GPL 2.0

Samo rozwiązanie nie jest niczym innym, jak wykorzystaniem biblioteki jQuery oraz dołączeniem do niego odpowiedniego jQuery plugin. Do tego dochodzą nam style CSS (ostylowanie naszego contentu) oraz kilka prostych skrytpów javascript (m.in. wykorzystywanych podczas resizowania strony).

To co potrzebujemy to pobrać najnowszą wersję biblioteki query oraz plik zip z demami blocksit

Moje rozwiązanie, które tutaj przedstawię, w dużej mierze opiera się na demie, które można pobrać z linku powyżej i jest to jedynie dostosowanie tego dema do aplikacji typu .net (asp.net / sharepoint).

Aby wykorzystać 'Blocksit' tworzymy nową stronę .aspx (może to być kontrolka .ascx), następnie do projektu dodajemy odpowiednie pliki:
- jquery-1.9.1.min (najnowsza wersja jquery w dniu 2013-03-01)
- blocksit.min (znajduje się w pliku z demem blocksit  - \blocksit-js\blocksit\demo2)
- style.css (znajduje się w pliku z demem blocksit - \blocksit-js\blocksit\demo2)
- zchpitblocksitdemo.js  (nasz plik ze skryptami, jakie będziemy utuchamiać na stronie)

Standardowo te skrypty były na stronie z demem, ale ja wolałem je przenieść do osobnego pliku .js i wyglądają one następująco:

$(document).ready(function () {
    //vendor script
    $('#header')
    .css({ 'top': -50 })
    .delay(1000)
    .animate({ 'top': 0 }, 800);

    $('#footer')
    .css({ 'bottom': -15 })
    .delay(1000)
    .animate({ 'bottom': 0 }, 800);

    //blocksit define
    $(window).load(function () {
        $('#container').BlocksIt({
            numOfCol: 5,
            offsetX: 8,
            offsetY: 8,
            blockElement: '.grid'
        });
    });

    //window resize
    var currentWidth = 1100;
    $(window).resize(function () {
        var winWidth = $(window).width();
        var conWidth;
        if (winWidth < 660) {
            conWidth = 440;
            col = 2
        } else if (winWidth < 880) {
            conWidth = 660;
            col = 3
        } else if (winWidth < 1100) {
            conWidth = 880;
            col = 4;
        } else {
            conWidth = 1100;
            col = 5;
        }

        if (conWidth != currentWidth) {
            currentWidth = conWidth;
            $('#container').width(conWidth);
            $('#container').BlocksIt({
                numOfCol: col,
                offsetX: 8,
                offsetY: 8
            });
        }
    });
});
 Ok. Przejdźmy zatem do omówienia najważniejszej części, czyli kodu strony .aspx / kontrolki .ascx:

Aby uzyskać ten layout, teoretycznie nie potrzebujemy żadnego 'code behind' i wystarczy nam czysty html, który docelowo powinien wyglądać podobnie jak na stronie z dema, czyli mniej więcej tak:

<link rel="stylesheet" href="/_layouts/style.css" type="text/css" />
<script src="/_layouts/jquery-1.9.1.min.js" type="text/javascript"></script>
<script src="/_layouts/blocksit.min.js" type="text/javascript"></script>
<script src="/_layouts//zchpitblocksitdemo.js" type="text/javascript"></script>
<div id="container">
    <div class="grid">
        <div class="imgholder">
            <img src="http://www.inwebson.com/demo/blocksit-js/demo2/images/img26.jpg" />
        </div>
        <strong>Bridge to Heaven</strong>
        <p>Where is the bridge lead to?</p>
        <div class="meta">by SigitEko</div>
    </div>
    <div class="grid">
        <div class="imgholder">
            <img src="http://www.inwebson.com/demo/blocksit-js/demo2/images/img15.jpg" />
        </div>
        <strong>Autumn</strong>
        <p>The fall of the tree...</p>
        <div class="meta">by Lars van de Goor</div>
    </div>
</div>
Aby jednak uzyskać, w pełni dynamicznie generowany przez nas layout, skorzystamy z kontrolki literala, czyli nasz kod html będzie wyglądał tak:

<link rel="stylesheet" href="/_layouts/style.css" type="text/css" />
<script src="/_layouts/jquery-1.9.1.min.js" type="text/javascript"></script>
<script src="/_layouts/blocksit.min.js" type="text/javascript"></script>
<script src="/_layouts//zchpitblocksitdemo.js" type="text/javascript"></script>

<div id="container">
    <asp:Literal runat="server" ID="ltrGrid" ></asp:Literal>
</div>
W tym przypadku, będziemy potrzebowali stworzyć 'code behind', który w najprostszym możliwym przypadku będzie wyglądał tak:

        protected void Page_Load(object sender, EventArgs e)
        {
            SetLiteralGrid();
        }
        private void SetLiteralGrid()
        {
            StringBuilder sb = new StringBuilder();

            sb.AppendLine("<div class='grid'>");
            sb.AppendLine("<div class='imgholder'>");
            sb.AppendLine("<img src='http://www.inwebson.com/demo/blocksit-js/demo2/images/img27.jpg' />");
            sb.AppendLine("</div>");
            sb.AppendLine("<strong>Sunset Lake</strong>");
            sb.AppendLine("<p>A peaceful sunset view...</p>");
            sb.AppendLine("<div class='meta'>by j osborn</div>");
            sb.AppendLine("</div>");

            ltrGrid.Text = sb.ToString();
        }
 Oczywiście w realnych scenariuszach interesuje nas wypełnienie contentem pełnej strony (a nie tylko pojedyńczego newsa), ale wtedy w 'code behind' wystarczy zwyczajnie wcześniej pobrać potrzebne dane i wrzucić je do StringBuildera za pomocą pętli.



niedziela, 24 lutego 2013

SPMonitoredScope, czyli sprawdzanie wydajności w aplikacjach sharepoint

Tematem aktualnego wpisu jest sposób, na sprawdzenie wydajności naszej aplikacji sharepoint, a dokładniej czasu jaki jest potrzebny, aby wyświetlić naszą aplikację w przeglądarce internetowej. Ponieważ temat wydajności w sharpoint jest bardzo obszerny, a z problemem zwiększania wydajności aplikacji sharepoint spotkał się niemal każdy doświadczony programista, dobrze jest mieć jakieś narzędzie, które zautomatyzuje proces sprawdzania wyników. Narzędzie, dzięki któremu będziemy wiedzieli, jaki dokładnie wpływ na wydajność mają zmiany w kodzie,  które wprowadzamy podczas procesu optymalizacji.

Tutaj z pomocą przychodzi nam SPMonitoredScope, które jest darmowym narzędziem służącym do lokalizacji wydajnościowego "wąskiego gardła". Jedynym ograniczeniem, jakie posiada SPMonitoredScope są rozwiązania typu "sandbox" ("piaskownica"), w których SPMonitoredScope nie działa.

Sposób wykorzystania SPMonitoredScope jest bardzo prosty:
Najpierw odpalamy konsolę (Start -> wyszukaj programy -> cmd.exe). W konsoli natomiast uruchamiamy program stsadm (ja uruchamiałem go z roota, do którego przechodzimy za pomocą komendy "cd..").  Dostępne komendy, możemy znaleźć na MSDN pod tym linkiem.  Osobiście preferuję opcję "on demand", czyli:

stsadm -o setproperty -pn developer-dashboard -pv ondemand 
Wyłączenie SPMonitoredScope następuje poprzez wpisanie w konsoli:

stsadm -o setproperty -pn developer-dashboard -pv off

Sprawdzanie konkretnych, wybranych partii kodu, pod kątem wydajności następuje, poprzez wykonanie naszego kodu wewnątrz "using-a".

using (new SPMonitoredScope("CallMethod1 Monitored Scope"))
{
    //TODO: tutaj będzie kod, który chcemy przetestować.
}
 
Wyniki sprawdzamy w przeglądarce, w nowej zakładce, która się pojawi, gdy tylko będziemy mieli aktywny SPMonitoredScope. Co do samych wyników, to dobrze jest stronę załadować przynajmniej kilka razy i nast. wyciągnąć średnią.

P.S. Link do bloga Tobiasa Zimmergrena, którego wpis stał stał się inspiracją dla mojego wpisu.

wtorek, 15 stycznia 2013

jQuery w Web Partach - zagrożenie wielokrotnego importu pliku jQuery

Jak każdy wie, aby móc wykorzystać bibliotekę jQuery w aplikachach webowych opartych o technologię .NET najpierw trzeba poinformować kompilator, że zamierzamy wykorzystać tą bibliotekę. Podobnie jak innymi plikami .js (javascript) przed pierwszym wykorzystaniem należy podać jej nazwę i miejce w którym się znajduje. Lokalizacja może być lokalna (zazwyczaj) lub zewnętrzna (np. do serwera projektu jQuery).

Zazwyczaj taka informacja znajduje się w nagłówku strony aspx (ewentualnie kontrolki ascx) na stronie z częścią HTML i wygląda następująco:
<script src="/_layouts/Scripts/jquery-1.8.2.min.js" type="text/javascript"></script>
Aby biblioteka jQuery działa poprawnie, na jednej stronie, może znajdować się tylko jedna deklaracja importująca plik z biblioteką jQuery. O ile w przypadku standardowych stron ASP.NET taka, pojedyncza deklaracja jest standardem, o tyle w przypadku Web Partów, które same w sobie są oddzielnymi aplikacjami, umieszczanymi niezależnie od siebie często występują konflikty importu. Każdy z web partów samodzielnie dodaje jQuery do strony, co w przypadku umieszczenia kilku takich web partów z deklaracjami importu na jednej stronie powoduje błędy w działaniu javascriptu na tej stronie.

Rozwiązanie polega na tym, aby plik z biblioteką jQuery wczytywać po stronie serwera w instrukcji warunkowej:

        protected void Page_Init(object sender, EventArgs e)
        {
            var cs = Page.ClientScript;

            if (!cs.IsClientScriptIncludeRegistered("jquery-1.8.2.min.js"))
                cs.RegisterClientScriptInclude("jquery-1.8.2.min.js",
                "/_layouts/Scripts/jquery-1.8.2.min.js");
        }
W podobny sposób możemy wczytać bibliotekę jQuery, gdy nie jest wczytana w postaci kodu javascript umieszczonego na stronie html:


<script type="text/javascript">

       var jQueryScriptOutputted = false;
       function initJQuery() {

             //if the jQuery object isn't available
             if (typeof (jQuery) == 'undefined') {

                    if (!jQueryScriptOutputted) {
                           //only output the script once..
                           jQueryScriptOutputted = true;

                           //output the script
                           document.write("<scr" + "ipt type=\"text/javascript\" src=\"/_layouts/Scripts/jquery-1.8.2.min.js\"></scr" + "ipt>");
                    }
                    setTimeout("initJQuery()", 50);
             }

       }
       initJQuery();

</script>

Takie  rozwiązanie pozwala nam na sytuację, aby wszystkie web party na stronie zaimportowały plik z biblioteką jQuery w wersji 1.8.2 tylko raz. Takie rozwiązanie nie jest jednak do końca idealne, ponieważ wymaga od nas, stosowania biblioteki w jednej i tej samej (najnowszej?) wersji we wszystkich web partach, a w przypadku umieszczenia na stronie web parta wymagającego nowszej wersji biblioteki jQuery przekompilowania i zaktualizowania wersji biblioteki w już istniejących web partach. Gdy tylko poznam sposób, w jaki można obejść te niedogodności postaram się tą informację tutaj umieścić.

niedziela, 25 listopada 2012

Metoda jQuery.ajax()

Jedną z największych zalet jQuery, jakie do tej pory miałem okazję wykorzystać, jest asynchroniczne odwołanie się kodu napisanego w javascript do metod, napisanych w C#. W najprostszej postaci (metoda javascript -> metoda c#) służy do tego jQuery.ajax.

Jak to działa?
Na stronie c# piszemy statyczną funkcję, którą oznaczamy atrybutem "Web Method", ktora w najprostszej postaci będzie wyglądać tak:

    public partial class WebMethodCommonHelper : LayoutsPageBase
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }
        [WebMethod]
        public static string GetActualDateTime()
        {
            return DateTime.Now.ToShortTimeString();
        }
    }

Odwołanie się do tej funkcji za pomocą javascriptu będzie natomiast wyglądało w ten sposób:

function GetDateTimeFromPage() {
  $.ajax({
                        type: 'POST',
                        url: 'WebMethodCommonHelper.aspx/GetActualDateTime',
                        data: JSON.stringify({ }),
                        contentType: 'application/json; charset=utf-8',
                        dataType: 'json',
                        success: function (data) {
                            var dateTimeFromPage =  data.d.Value
                            setDateTime(dateTimeFromPage);
                        }
                    });
}

function setDateTime(dateTimeToSet)
{
//TODO: tutaj wykorzystujemy date, pobrana ze strony
}

Krótki opis, kodu, który widzimy powyżej:
a) Tworzymy stronę aspx, na której umieszamy statyczną web methodę z atrybutem [Web Method]. To ta metoda jest odpowiedzialna za "część serwerową".
b) po stronie javascriptu tworzmy 2 metody, o nazwach "GetDateTimeFromPage()" oraz "setDateTime". Jedna będzie odpowiedzialna, za pobranie danych z metody c# o nazwie "GetActualDateTime", natomiast druga będzie wykorzystywała te dane bezpośrednio na stronie ( "setDateTime").
 Metoda $.ajax w najprostszej formie, pobiera 4 parametry:
- type: 'POST' -> tryb pracy protokołu http (POST lub GET)
- url - adres naszej web methody, poprzedzony adresem do strony aspx na której się znajduje (jeżeli strona jest w odpowiednim katalogu, jak to często bywa np. w aplikacjach sharpoint, to wtedy adres może wyglądać np tak: '/_layouts/Branding/Common/WebMethodCommonHelper.aspx/GetActualDateTime'
- data - parametry, jakie przekazujemy do web methody z poziomu javascriptu (zostaną opisane później przy bardziej zaawansowanym przypadku)
-contentType: 'application/json; charset=utf-8' - kodowanie (tego zazwyczaj nie zmieniamy)
- dataType: 'json' - rodzaj danych (tego zazwyczaj nie zmieniamy)
- success: function (data) {
                           var dateTimeFromPage =  data.d;
                            setDateTime(dateTimeFromPage);
                        }
- fragment kodu, odpowiedzialny, za to, co powinno się zadziać, w przypadku, gdy metoda wykona się poprawnie. W naszym przypadku, tworzymy zmienną  'dateTimeFromPage' i przypisujemy do niej wartości, jakie zwróciła nam nasza 'Web Method', a nast. przekazujemy tą wartość do metody pomocniczej, która wykorzysta ją dalej w naszym kodzie.
Co ważne, dane które dostajemy z powrotem to: "data.d". Dla typów wbudowanych, np. 'int32', 'string' itp. dane zwracane są jako "data.d". Dla klas, należy się odwołać to zmiennych wewnątrz klasy (jeżeli klasa, którą zwracamy ma publiczną własciwość o nazie "wartosc" to odwołamy się do niej poprzez var wartosc = data.d.wartosc").

Ok, skoro najprostszy przykład mamy już za sobą, to teraz spróbujmy jakiś bardziej zaawansowany przykład, np. z wykorzystaniem sharepointa:
a) kod javasctipt

function getCurrency()
{
                    var urlLink = window.location.protocol + "//" + window.location.host + _spPageContextInfo.siteServerRelativeUrl; //_spPageContextInfo.webServerRelativeUrl;
                    $.ajax({
                        type: 'POST',
                        url: '/_layouts/Branding/Common/WebMethodCommonHelper.aspx/GetCurrency',
                        data: JSON.stringify({ url: urlLink }),
                        contentType: 'application/json; charset=utf-8',
                        dataType: 'json',
                        success: function (data) {
                            var currency = parseFloat(data.d.Value);
                            setCurrency(currency);
                        }
                    });
}
function setCurrency(currency)
{
 //TODO: tutaj ustawimy przelicznik
}

b) kod c#
     public partial class WebMethodCommonHelper : LayoutsPageBase
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }
        [WebMethod]
        public static CurrencyHelper GetCurrency(string url)
        {
            using (SPSite oSiteCollection = new SPSite(url))
            {
                using (SPWeb web = oSiteCollection.OpenWeb())
                {
                    double rate = 4.56; //CurrencyHelper.GetRate(web);
                    CurrencyHelper lineHelper = new CurrencyHelper() { Value = rate, HfName = "Rate" };
                    return lineHelper;
                }
            }
        }
        [Serializable]
        public class CurrencyHelper
        {
            public string HfName { get; set; }
            public double Value { get; set; }
        }
}

Ok. Przejdźmy do analizy kodu:
Tym razem najpierw tworzymy metodę javascript. Ponieważ będziemy wykorzystywać sharpointa, bedziemy potrzebowali utworzyć obiekty SpSite oraz SpWeb. W tym celu, wykorzystamy zmienne shapointowe dostępne w języku javascript (tylko dla sharepoint 2010 lub nowszego). Mam na myśli:
_spPageContextInfo.siteServerRelativeUrl - zwraca adres SpSite
_spPageContextInfo.webServerRelativeUrl - zwraca adres SpWeb
Z pomocą tych zmiennych tworzymy pełny adres http, strony jaka nas interesuje (w zależności od tego, na której witrynie znajduje się lista do której zamierzamy się odwołać, czy na bierzącej witrynie, czy na 'root webie').
Mają odpowiedni adres http przekazujemy go do metody jQuery.ajax jako parametr url (ważne, aby nazwa parametru w jQuery.ajax oraz metodzie c# były takie same).
Jako rezultat otrzymujemy klasę napisaną przez użytkownika, więc w ceku odczytania wartości odwołujemy się do jej publicznej właściwości (Value), którą nast. przekazujemy do metody docelowej.

W przypadku kodu c#, ponownie tworzymy metodę statyczna z adrybutem "Web Method", oraz parametrem "url". Wykorzystując parametr, otwieramy SpSite oraz SpWeb'a, a nast. wykonujemy na nim interesujące nas operacje. Jako wynik, zwracamy obiekt stworzonej przez nas klasy CurrencyHelper.


p.s. Należy pamiętać, o dołączeniu odpowiednich referencji, tj. pliku z biblioteką "jQuery" po stronie javascriptu, oraz odpowiednich namespece po stronie c#.

sobota, 27 października 2012

Podniesienie uprawnień sharepoint

Bardzo często podczas pisania programów w technologii sharepoint (np. Visual Web Part) zdarza się konieczność czasowego podniesienia uprawnień użytkownika dla wykonania konkretnej czynności. Przykładem, mogą być uprawnienia do listy. Normalny użytkownik posiada uprawnienia tylko do odczytu elementów listy (elementy listy sharpoint są podobne do rekordów w bazie danych), a co za tym idzie nie posiada uprawnień do tworzenia/modyfikowania elementów listy. Aby taki użytkownik mógł utworzyć/zmodyfikować element na takiej liście za pomocą napisanej przez nas aplikacji (np. mógł utworzyć nową 'fakturę' za pomocą aplikacji, do zarządzania fakturami, opartej na listach sharepoint), trzeba czasowo podnieść jego uprawnienia. Podniesienie uprawnień w kodzie C# w praktyce oznacza pobranie SPSite z podwyższonymi uprawnieniami (tzw. SuperSite).
Przykład:



SPSecurity.RunWithElevatedPrivileges(delegate()
{
    using (SPSite
superSite = new SPSite(SPContext.Current.Site.ID))
    {
        using (SPWeb
superWeb = superSite.OpenWeb(SPContext.Current.Web.ID))
        {
            // Your code here
        }
    }
});
 
Podczas pobierania SuperSite lepiej jest jednak wykorzystać wrapper, w postaci klasy SPSecurityHelper, zademonstrowanej przez Dana Larsona na jego tech-blogu
Klasa SPSecurityHelper:

using Microsoft.SharePoint;


    /// <summary>A class for working with elevated privilege</summary>
    public static class SPSecurityHelper
    {
        /// <summary>Returns an elevated site</summary>
        /// <param name="theSite">
        /// The site that you want an elevated instance of.
        /// You must dispose of this object unless it is part of SPContext.Current.
        /// </param>
        /// <returns>An elevated site context.</returns>
        /// <remarks>Be sure to dispose of objects created from this method.</remarks>
        public static SPSite GetElevatedSite(SPSite theSite)
        {
            var sysToken = GetSystemToken(theSite);
            return new SPSite(theSite.ID, sysToken);
        }

        /// <summary>Gets a UserToken for the system account.</summary>
        /// <param name="site"></param>
        /// <returns>A usertoken for the system account user./returns>
        /// <remarks>Use this token to impersonate the system account</remarks>
        public static SPUserToken GetSystemToken(SPSite site)
        {
            site.
CatchAccessDeniedException = false;
            try
            {
                return site.SystemAccount.UserToken;
            }
            catch (UnauthorizedAccessException)
            {
                SPUserToken sysToken = null;

                // Only use runwithelevated to grab the system user token.
                SPSecurity.RunWithElevatedPrivileges(
                    delegate()
                    {
                        using (SPSite lolcatKiller = new SPSite(site.ID))
                        {
                            sysToken = lolcatKiller.SystemAccount.UserToken;
                        }
                    }
                );
                return sysToken;
            }
        }
    }
 Wykorzystanie klasy SPSecurityHelper:

 using (SPSite superSite = SPSecurityHelper.GetElevatedSite(SPContext.Current.Site))
{
    using (SPWeb superWeb = superSite.OpenWeb(SPContext.Current.Web.ID))
    {
               // Your code here
    }
}

 Sposób wykorzystania jest bardzo prosty, tzn. do klasy SPSecurityHelper przekazujemy obiekt zwykłego SpSite, natomiast z powrotem dostajemy obiekt tego samego SpSite z podwyższonymi uprawnieniami. Dzięki wykorzystaniu klasy SPSecurityHelper kod jest krótszy oraz lepszy jakościowo.

Adres komputera, korzystającego z naszej strony

    Uzyskanie informacji nt. komputera, z którego wykonano jakąś czynność jest bardzo ważne, z punktu widzenia bezpieczeństwa. Informacja ta, jest ważna zarówno w przypadku systemów wewnętrznych (intranet), jak i systemów zewnętrznych (internet).
    W przypadku intranetu, jednym ze szczególnych przypadków jest "usuwanie rekordów z bazy danych" (np. usuwanie faktur w systemie księgowym), gdzie oprócz loginu osoby wykonującej akcję (dost. z systemu), chcemy mieć dla pewności informacje nt. komputera, z którego wykonano akcję.
    W przypadku internetu, chcemy mieć zazwyczaj informacje, nt. komputera, który próbuje się włamać do naszego systemu, lub wykonuje inną nieporządaną akcję (np. atak DDoS).

W jaki sposób uzyskać informację nt. komputera, korzystającego z naszego systemu? Wykorzystamy do tego celu klasy z przestrzeni nazw "System.Web" oraz "System.Net".

using System.Web;
using System.Net;
IPHostEntry hostEntry = Dns.GetHostEntry(HttpContext.Current.Request.ServerVariables["REMOTE_HOST"].ToString());

Praktyczny sposób wykorzystania zmiennej hostEntry prezentuje przykład, wykonany na zwykłej stronie asp.net (web forms):


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web;
using System.Net;
using System.Text;

public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string ComputerName = "";
        IPAddress[] IpAdressList = null;
        StringBuilder sb = new StringBuilder();
        try
        {
            IPHostEntry hostEntry = Dns.GetHostEntry(HttpContext.Current.Request.ServerVariables["REMOTE_HOST"].ToString());
            ComputerName = hostEntry.HostName.ToString();
            IpAdressList = hostEntry.AddressList;
        }
        catch { }
        sb.AppendLine(string.Format("Nazwa komputera {0}", ComputerName));
        if (IpAdressList != null)
        {
            for(int i=0; i < IpAdressList.Length; i++)
            {
                sb.AppendLine(string.Format("Typ adresu IP {0}, adres IP {1}", IpAdressList[i].AddressFamily, IpAdressList[i].ToString()));
            }
        }
        string computerInfo = sb.ToString();
    }
}

   Posiadając informacje nt. komputera, możemy te informacje zapisać do bazy danych, lub do logów. W przypadku aplikacji asp.net preferuję wykorzystania gotowego rozwiązania, jakim jest log4net, natomiast w przypadku Sharepoint 2010 logowanie do natywnych logów sharepointa (jak sie to robi opisze w jednym z przyszłych wpisów).

Edit: Niestety te metody w praktyce nie okazały się na tyle dobre, jak być powinne. Nowe metody:

        public static string GetUser_IP()
        {
            string VisitorsIPAddr = string.Empty;
            if (HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != null)
            {
                VisitorsIPAddr = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"].ToString();
            }
            else if (HttpContext.Current.Request.UserHostAddress.Length != 0)
            {
                VisitorsIPAddr = HttpContext.Current.Request.UserHostAddress;
            }
            return VisitorsIPAddr;
        }
        public static string GetUserIP()
        {
            string ipList = HttpContext.Current.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
            if (!string.IsNullOrEmpty(ipList))
            {
                return ipList.Split(',')[0];
            }
\            return HttpContext.Current.Request.ServerVariables["REMOTE_ADDR"];
        }
Używanie
        string visitorIp = GetUser_IP();
        int intAddress = BitConverter.ToInt32(IPAddress.Parse(visitorIp).GetAddressBytes(), 0);

środa, 10 października 2012

Pobieranie aktualnego kursu walut z NBP

Bardzo często, podczas pisania aplikacji finansowo-księgowych zdarza nam się potrzeba przeliczania walut po ich aktualnym kursie. O ile w przypadku naprawdę dużych systemów klient najprawdopodobniej narzuci nam własne kursy (które będziemy importować np. z SAP), o tyle w przypadku pisania małych i średnich systemów przeliczanie walut po aktualnym kursie będziemy musieli wykonać samemu. Z pomocą przychodzi nam tutaj Narodowy Bank Polski, który codziennie pomiędzy godź. 10.30-12.30 wystawia plik XML z aktualnymi kursami na dany dzień. Co ważne, robi to w sposób podmiany starego pliku, tzn. że do godź. ~10.30 istnieje plik XML z wczorajszym kursem, po czym pomiędzy godzinami 10.30-12.30 zastępowany jest on nowym plikiem, o takiej samej nazwie, pod tym samym linkiem, ale z aktualnym kursem na dany dzień. Wspomniany plik XML można znaleźć pod tym linkiem.

Dzięki temu linkowi, będziemy znali aktualne kursy NBP, po których będziemy mogli dokonywać przeliczeń pomiędzy walutami po aktualnym kursie wg. NBP. Aby nieco uprościć całą sprawę, napisałem metodę, która pobiera zawartość tego pliku i udostępnia kursy w bardziej przystępnej, obiektowej formie. Mając tą metodę, możemy później napisać "timer job" (sharepoint timer job, lub aplikację konsolową uruchamianą za pomocą Windows Task Scheduler), który będzie umieszczał zawartość tego pliku w bazie danych, liście sharepointa, czy gdzie tylko chcemy.

Metoda, która pobiera aktualny kurs wygląda tak:
        private static List<Currency> ImportData(string url)
        {
            // Creates an HttpWebRequest with the specified URL.
            HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
            // Sends the HttpWebRequest and waits for the response.           
            HttpWebResponse myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse();
            // Gets the stream associated with the response.
            Stream receiveStream = myHttpWebResponse.GetResponseStream();
            Encoding encode = System.Text.Encoding.GetEncoding("iso-8859-2");
            // Pipes the stream to a higher level stream reader with the required encoding format.
            StreamReader readStream = new StreamReader(receiveStream, encode);
            string readedData = readStream.ReadToEnd();
            myHttpWebResponse.Close();
            // Releases the resources of the Stream.
            readStream.Close();
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(readedData);
            XmlNodeList currencyListXml = xmlDoc.GetElementsByTagName(Constraints.POSITION);
            var date = xmlDoc.GetElementsByTagName(Constraints.PUBLICATION_DATE);
            DateTime currencyDate = Convert.ToDateTime(date[0].FirstChild.Value);

            List<Currency> currencyList = new List<Currency>();
            foreach (XmlNode currencyXml in currencyListXml)
            {
                //XmlNodeList nodes = currencyXml.ChildNodes;
                Currency currency = new Currency();
                currency.CurrencyName = currencyXml.ChildNodes[0].FirstChild.Value;
                currency.Converter = currencyXml.ChildNodes[1].FirstChild.Value;
                currency.CurrencyCode = currencyXml.ChildNodes[2].FirstChild.Value;
                currency.CurrencyRate = Convert.ToDouble(currencyXml.ChildNodes[3].FirstChild.Value);
                currency.CurrencyDate = currencyDate;
                currencyList.Add(currency);
            }
            return currencyList;
        }
 Jako parametr podajemy adres url do pliku XML z listą walut. Wykorzystano parametr, ponieważ NBP wystawia kilka, różnych plików XML z aktualnymi kursami walut (LastA.xml, LastB.xml, LastC.xml) w zależności od tego, ile i które waluty nam są potrzebne korzystamy z odpowiedniego linku (LastA.xml podany w aplikacji pobiera 37 najpopularniejszych walut).

 Sama metoda, nie jest zbyt skomplikowana. Podajemy na wejście adres url, nast. za pomocą obiektów HttpWebRequest oraz HttpWebResponse pobieramy zawartość w pliku XML jako stream. Następnie tworzymy z niego XmlDocument, z którego po kolei wydobywamy interesujące nas dane i przekazujemy je do obiektów stworzonej przez nas klasy 'Currency'. Na zakończenie metoda zwraca listę obiektów 'Currency', na której to później dużo łatwiej jest nam operować.

Przykładowy program, który pobiera aktualne kursy walut, a nast. wyświetla je na konsoli wygląda następująco:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Xml;

namespace GetAcctualNBPCurrency
{
    class Program
    {
        static void Main(string[] args)
        {
            string url = @"http://www.nbp.pl/kursy/xml/LastA.xml";
            List<Currency> currencyList = ImportData(url);

            foreach (Currency currency in currencyList)
            {
                Console.WriteLine("Kurs dla waluty {0} ({1}) na dzień {2} wynosi: {3}", currency.CurrencyCode, currency.CurrencyName, currency.CurrencyDate.ToShortDateString(), currency.CurrencyRate);
            }
            Console.ReadKey();
        }
        private static List<Currency> ImportData(string url)
        {
            // Creates an HttpWebRequest with the specified URL.
            HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url);
            // Sends the HttpWebRequest and waits for the response.           
            HttpWebResponse myHttpWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse();
            // Gets the stream associated with the response.
            Stream receiveStream = myHttpWebResponse.GetResponseStream();
            Encoding encode = System.Text.Encoding.GetEncoding("iso-8859-2");
            // Pipes the stream to a higher level stream reader with the required encoding format.
            StreamReader readStream = new StreamReader(receiveStream, encode);
            string readedData = readStream.ReadToEnd();
            myHttpWebResponse.Close();
            // Releases the resources of the Stream.
            readStream.Close();
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(readedData);
            XmlNodeList currencyListXml = xmlDoc.GetElementsByTagName(Constraints.POSITION);
            var date = xmlDoc.GetElementsByTagName(Constraints.PUBLICATION_DATE);
            DateTime currencyDate = Convert.ToDateTime(date[0].FirstChild.Value);

            List<Currency> currencyList = new List<Currency>();
            foreach (XmlNode currencyXml in currencyListXml)
            {
                //XmlNodeList nodes = currencyXml.ChildNodes;
                Currency currency = new Currency();
                currency.CurrencyName = currencyXml.ChildNodes[0].FirstChild.Value;
                currency.Converter = currencyXml.ChildNodes[1].FirstChild.Value;
                currency.CurrencyCode = currencyXml.ChildNodes[2].FirstChild.Value;
                currency.CurrencyRate = Convert.ToDouble(currencyXml.ChildNodes[3].FirstChild.Value);
                currency.CurrencyDate = currencyDate;
                currencyList.Add(currency);
            }
            return currencyList;
        }
    }
    class Currency
    {
        public string CurrencyName { get; set; }
        public string Converter { get; set; }
        public string CurrencyCode { get; set; }
        public double CurrencyRate { get; set; }
        public DateTime CurrencyDate { get; set; }

        public Currency() { }
    }
    class Constraints
    {
        public static readonly string PUBLICATION_DATE = "data_publikacji";
        public static readonly string POSITION = "pozycja";
        public static readonly string CURRENCY_NAME = "nazwa_waluty";
        public static readonly string CONVERETER = "przelicznik";
        public static readonly string CURRENCY_CODE = "kod_waluty";
        public static readonly string CURRENCY_RATE = "kurs_sredni";
    }
}
P.S. Zasady korzystania z danych publikowanych na stronie NBP można znaleźć w statusie serwisu pod tym adresem.