Ograničavanje broja konekcija na SQL Server

Više puta mi se pojavila potreba da kontrolišem broj konekcija iz aplikacije na bazu podataka, samim tim i da ograničim pristup u odredjenim situacijama.

Na primer, ukoliko pravite program koji registrujete na odredjeni/dozvoljni broj konekcija, ili jednostavno prateći poslovnu logiku želite da zabranite korisniku da bude prijavljen na više mesta, ovo je rešenje koje možete da iskoristite.

SELECT  sistem.loginame,
        sistem.hostname,
		sistem.program_name
FROM    master..sysprocesses AS sistem
WHERE	sistem.hostname <> ''

Ovo će nam dati sledeći rezultat

Ovim upitom vidimo aktivne konekcije na bazu podataka/server, što nam daje neke potrebne informacije za početak, ali definitivno nedovoljne da definišemo i ograničimo pristup po potrebama.

Kao što vidite na rezultatu upita, pročitali smo

  1. LoginName
  2. HostName
  3. ProgramName

E, sad, ono što je jedan od najbitnijih podataka u celoj ovoj priči je, koja je to aplikacija otvorila konekciju? To možemo da vidimo iz kolone “program_name”.
Naravno, da bismo mogli da razgraničimo šta je “naša” konekcija, a šta je neki sistemski proces, treba nam naziv programa koji je “otvorio” konekciju. Iz rezultata primera vidimo da dve konekcije “drži” MSSQL, i treća je otvorena od programa “GHC”.

Kako definisati naziv programa u konekciji? Lako!
Jednostavno je potrebno dodeliti property na kreiranju ConnectionString-a kao u primeru:

private string CreateConnectionString(string server, string database, string username, string password)
{
        SqlConnectionStringBuilder cb = new SqlConnectionStringBuilder();
        cb.DataSource = server;
        cb.InitialCatalog = database;
        cb.MultipleActiveResultSets = true;
        cb.UserID = username;
        cb.Password = password;
        cb.Pooling = true;
        // Vrednost naziva ne sme biti NULL, pa obratite paznju ako ga prosledjujete kao parametar
        cb.ApplicationName = System.Reflection.Assembly.GetEntryAssembly().GetName().Name;
        return cb.ToString();
}
U primeru smo iskoristili Reflection da dobijemo ime programa, vi ga možete proslediti metodi kao dodatni parametar, ali vodite računa da nije NULL, jer ćete dobiti Exception za to, ApplicationName property ne može biti NULL!

Dobro, sada imamo naziv programa, server login i ime računara sa koga je konekcija kreirana, i dalje nedovoljno…

Ono što fali je da znamo da je to naš korisnik.

Mi smo to rešavali na sledeći način. Tačnije, logika po kojoj smo uvek pravili security na aplikaciji nam se poklopila sa ovakvim zahtevima.

Naime, ono što je važno je da naša baza korisnike “mapira” na server login-e, što u praksi znači da će za svakog korisnika aplikacije, biti kreiran Sql Server Login sa istim username-om.

Ne zbog ovog ograničavanja, to dalje širi priči i otvara dosta dobrih mogućnosti kasnije, kao npr. kreiranje rola na bazi i automatsko dodeljivanje user-a roli, samim tim postavlljanje predefinisanog seta privilegija na tabele, procedure, itd…

Dakle, kada smo ustanovili da imamo korisnike u našoj bazi paralelno sa SQL Server login-ima, ostaje nam da malo promenimo upit da nam vrati podatke o našim korisnicima.

Pretpostavimo da je bazi postoji tabela “User”, koja u sebi, izmedju ostalog, ima i kolonu “Username”, koju koristimo za identifikaciju korisnika svakako. Vrednost kolone “Username” odgovara SQL Server login-u sa istim “LoginName”-om. Sada upit izgleda ovako:

SELECT  u.FirstName,
        u.LastName,
        sistem.loginame,
        sistem.hostname,
		sistem.program_name
FROM    master..sysprocesses AS sistem INNER JOIN
        [User] as u ON u.Username COLLATE DATABASE_DEFAULT = sistem.loginame COLLATE DATABASE_DEFAULT
WHERE   hostname <> ''

I rezultat je ono što nam treba:

I za kraj još jedna mala napomena i objašnjenje.

Obratite pažnju na COLLATION servera i baze podataka. Ovim upitom su JOIN-ovane tabele iz “master” baze i vaše baze, čiji COLLATION-i mogu biti različiti, što će dovesti do ozbiljnih problema pri pravljenju upita koji u sebi imaju npr. WHERE klauzulu, jer server neće moći da uporedi string-ove na različitim COLLATION-ima.

Zato koristimo COLLATE DATABASE_DEFAULT u upitu, što rešava ove probleme i dozvoljava da se upit izvrši iako kolacije nisu iste.
Pogledajte blog Pinal Dave-a za više informacija o problemu sa COLLATION-ima (i svemu ostalom vezano za SQL Server), da ja ne pričam ispričane priče.

Ovako ćete u svakom momentu imati mogućnost da proverite koliko konekcija je otvoreno ka serveru i od strane kojih korisnika, ostaje Vam da pustite mašti na volju, da napravite npr. neku lepu formu koja će korisnika nakon prijave obavestiti da je negde već prijavljen ili da je broj dozvoljenih konekcija prekoračen i sl.

Spring MVC Security konfiguracija

Na samom startu svake Spring MVC web aplikacije susrećemo se sa konfiguracijom i pisanjem bean-ova koji ćemo kasnije koristiti.
Jedna od stvari koje ta konfiguracija sadrži su i security bean-ovi.
Dobra stvar Spring-a je da možemo da imamo definisanu sigurnost aplikacije na nivou bean-ova, bez pisanja dodatnog koda u Java klasama. Kad kažem sigurnost, mislim na login i autorizaciju korisnika za korišćenje aplikacije koja takvu funkcionalnost zahteva, naravno.

Spring MVC aplikacija ća automatski instancirati sve klase koje se nalaze u bean-ovima, tako da one ostaju spremne za korišćenje u Javi, što je, ispostavilo se, dosta praktično, i uveliko smanjuje broj napisanih linija koda.

Osnovna struktura aplikacije može izgledati npr. ovako:

1. servlet-context.xml – Osnovni konfiguracioni fajl

Prvo sto moramo da kreiramo svakako je servlet-context.xml.
On, u jednostavnoj varijanti potrebnoj za ovu priču, može izgledati ovako:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc 
                        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
                        http://www.springframework.org/schema/beans 
                        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/context 
                        http://www.springframework.org/schema/context/spring-context-3.0.xsd
                        ">
                        
	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	<context:component-scan base-package="com.ghcpro" />

	<!-- Configures the @Controller programming model -->
	<mvc:annotation-driven />

	<!-- Forwards requests to the "/" resource to the "welcome" view -->
	<mvc:view-controller path="/" view-name="home" />
	<!-- Handles HTTP GET requests for /resources/** by efficiently serving 
		up static resources in the ${webappRoot}/resources directory -->
	<mvc:resources mapping="/resources/**" location="/resources/" />

	<!-- MySQL Configuration -->
	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url" value="jdbc:mysql://localhost:3306/db_name" />
		<property name="username" value="root" />
		<property name="password" value="" />
	</bean>
    
    <!-- Transaction manager -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

Obavezan je DataSource bean, njega će Spring koristiti da bi se povezao na bazu i pročitao podatke o korisnicima.

	
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<property name="driverClassName" value="com.mysql.jdbc.Driver" />
	<property name="url" value="jdbc:mysql://localhost:3306/db_name" />
	<property name="username" value="root" />
	<property name="password" value="" />
</bean>

Ostalo u servlet-context.xml fajlu su manje ili više standarne bean-ovi Spring aplikacije. Vi ćete svakako svoj proširiti i prilagoditi svojim potrebama.

2. security-context.xml – Konfiguracija Spring Security-a

Kada smo kreirali servlet-context.xml, biće nam potreban još jedan, security-context.xml.

Generalno, sve ovo može da bude definisano i u jednom xml-u, al radi bolje preglednosti, ipak ćemo ih razdvojiti u posebne fajlove.

Ovako izgleda security-context.xml

<beans:beans xmlns="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.0.3.xsd">

	<http use-expressions="true" auto-config="true" >
	    <intercept-url pattern="/admin/**" access="hasRole('ROLE_ADMIN')"  />
		<intercept-url pattern="/login*" filters="none" />
		<intercept-url pattern="/resources/**" filters="none" />
		<intercept-url pattern="/**" access="hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')" />
		<form-login login-processing-url="/j_spring_security_check" always-use-default-target="true" login-page="/login" default-target-url="/" authentication-failure-url="/loginfailed" />
		<logout logout-url="/logout" logout-success-url="/" />
		<access-denied-handler error-page="/access-denied"/>
	</http>

	<authentication-manager>
		<authentication-provider>
			<jdbc-user-service data-source-ref="dataSource"
				users-by-username-query="select email as username, password, active as enabled from users where email=?"
				authorities-by-username-query="select u.email as username, concat('ROLE_', u.role) as authority from users u where u.email=?" />
		</authentication-provider>
	</authentication-manager>

</beans:beans>

O ovom xml-u definišemo prava pristupa delovima aplikacije, kao i inicijalnu autorizaciju korisnika.

Obratite pažnju na nekoliko stvari:

  1. Akcije u Spring VMC kontrolerima treba da odgovaraju definiciji u security bean-u
    <form-login login-processing-url="/j_spring_security_check" always-use-default-target="true" login-page="/login" default-target-url="/" authentication-failure-url="/loginfailed" />
    <logout logout-url="/logout" logout-success-url="/" />
    <access-denied-handler error-page="/access-denied"/>
    

    što podrazumeva da će vaš npr. HomeController imati definisane akcije “login”, “loginfailed”, “access-denied” i “logout” url-ove, kako bi Spring znao da pravilno redirektuje određene zahteve. Naravno, prilagođavate po svojoj potrebi, ovo napominjem da bih ispratio konfiguraciju iz primera.

  2. Role moraju početi sa “ROLE_”. Dakle, mi ovde u primerima imamo ROLE_USER i ROLE_ADMIN (koje ćete koristiti ukoliko želite da razdvojite delove aplikacije na npr. deo za korisnike i deo za administratore). Za početak je ovo sasvim dovoljno. U ovom primeru vidimo kreiranje select upita da bismo dobili rezultat koji će Spring “razumeti”:
    concat('ROLE_', u.role) as authority
  3. SQL select upiti moraju imati naziv kolona kakav Spring očekuje, Ukoliko to nije slučaj, koristite alias-e kolona, kao ja u ovom primeru.

VAŽNO!

O ovom primeru je aplikacija konfigurisana tako da ne dozvoljava pristup bilo kom delu sajta ukoliko se korisnik nije prijavio, pored toga, deli aplikaciju na korisnički i administratorski deo. Ukoliko to nije potreba, izmenite “intercept-url” delove po želji.
Na primer, da biste dozvolili pristup kompletnom sajtu promenite intercept-url pravila u npr.

<intercept-url pattern="/**" filters="none" />

Prilagodite ostala “intercept-url” pravila po želji.
I obratite pažnju na redosled definisanja pravila! Spring će prestati da upoređuje pravila sa datim URL-um čim naiđe na prvo pravilo koji odgovara URL-u.

3. Login forma

I na kraju nam ostaje da napravimo login formu koja će se povezati sa Spring security-em.

Osnovna login forma izgleda ovako:

<c:if test="${not empty error}">
		<div class="error">
			Login failed. Please check your credentials and try again.<br /> 
			System error: ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
		</div>
	</c:if>

	<form name='f' action="<c:url value='/j_spring_security_check' />" method='POST'>
		<table>
			<tr>
				<td>Email:</td>
				<td><input type='text' name='j_username' value=''>
				</td>
			</tr>
			<tr>
				<td>Password:</td>
				<td><input type='password' name='j_password' />
				</td>
			</tr>
			<tr>
				<td colspan="2" align="right"><input class="button blue" name="submit" type="submit" value="Login" />
				</td>
			</tr>
		</table>
	</form>

I Out-of-Box Spring Security je spreman.

Nadam se da će nekome ovo biti od pomoći.

Kreiranje objekta sa RowMapper-om

Ideja je da kreiramo jedostavan, precizan i uniforman način za mapiranje/čitanje objekata iz baze, a da pritom ne koristimo gotove framework-e.

Idealan način, bar za mene, su RowMapper klase.

Ovo nije ništa novo, RowMapper-i se koriste u Javi kao podrazumevani, bar koliko sam ja uspeo da primetim.

Ono sto mi nije jasno, zašto ih ja nisam koristio ranije? Definitivno skraćuju vreme pisanja, osiguravaju precizan kod, klase su male i fokusirane, baš sve kako treba.

Za početak nam treba interface koji ćemo koristiti kako bismo “predali” određeni RowMapper klasi za čitanje podataka iz baze.

using System.Data;

namespace GHC.Data
{
    /// <summary>
    /// Map data reader values to a new instance of type T
    /// </summary>
    public interface IRowMapper
    {
        T Map(IDataRecord record);
    }
}

Konkretne stvari

Da bih najbolje objasnio o čemu se radi, uzeću za primer objekat City. Dakle, cilj je jasan, čitamo podatke o gradovima iz baze.

Evo ga model:

using System;

namespace GHC.Models
{
    public class City
    {
        public Guid ID { get; set; }
        public string Name { get; set; }
        public string PostalCode { get; set; }
        public override string ToString()
        {
            return this.PostalCode + " " + this.Name;
        }
    }
}

Za svaki model ćemo kreirati odgovarajuću RowMapper klasu koja će implementirati prethodno kreirani IRowMapper interfejs.

Možda Vam izgleda kao preterano, ali taj kod svakako pišete, samo što ga u ovom slučaju držimo u posebnim klasama. Da ponovim, zadržaćemo male, fokusirane klase, automatski smanjujemo mogućnost greške, “gubljenja” po kodu i pisanje čuvenog “špageti koda”

Ovako izgleda RowMapper klasa za model City:

using GHC.Data;
using GHC.Models;

namespace GHC.RowMappers
{
    public class CityRowMapper : IRowMapper
    {
        public City Map(System.Data.IDataRecord record)
        {
            return new City()
            {
                ID = record.GetGuid(0),
                Name = record["Name"].ToString(),
                PostalCode = record["PostalCode"].ToString(),
                State = new State() { ID = record.GetGuid(3) }
            };
        }
    }
}

Ono što se vidi i što stvari pojednostavljuje, je to sto implementacija IRowMapper-a po definiciji vraća zadati tip, tako da kasnije nema nikakvih dodatnih konvertovanja i suvišnog koda.

Connector – a gde su ti podaci?

Došli smo do još konkretnijih stvari.

Kreiramo klasu Connector koja će nam služiti za povezivanje sa bazom i vraćanje podataka preko RowMapper-a

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using Microsoft.SqlServer.Management.Smo;

namespace GHC.Data
{
    public class Connector
    {
		private string _connectionString;

		public string Server { get; set; }
		public string Database { get; set; }
		public string Username { get; set; }
		public string Password { get; set; }

		public string ConnectionString
		{
			get
			{
				if (string.IsNullOrEmpty(_connectionString))
				{
					_connectionString = CreateConnectionString(this.Server, this.Database, this.Username, this.Password, null, 0);
				}

				return _connectionString;
			}
		}

		private string CreateConnectionString(string server, string database, string username, string password, int connectionTimeout)
		{
			SqlConnectionStringBuilder cb = new SqlConnectionStringBuilder();
			cb.DataSource = server;
			cb.InitialCatalog = database;
			cb.MultipleActiveResultSets = true;
			cb.UserID = username;
			cb.Password = password;
			cb.Pooling = true;

			if (connectionTimeout >= 0)
			{
				cb.ConnectTimeout = connectionTimeout;
			}

			return cb.ToString();
		}

		///<summary>
		/// Execute reader and return first instance of object created with IRowMapper type to instantiate
        	/// </summary>
		/// Sql Query to retreive object from database
	        /// IRowMapper used to map object type from reader
        	/// List of T
        	public T Get<T>(string query, IRowMapper rowMapper)
        	{
            		object obj = null;

			using (SqlConnection Connection = new SqlConnection(ConnectionString))
			{
				try
				{
					Connection.Open();

					using (SqlCommand command = Connection.CreateCommand())
					{
						command.CommandText = query;

						SqlDataReader reader = command.ExecuteReader();

						e.Reader = reader;

						reader.Read();

						if (reader.HasRows)
						{
							obj = rowMapper.Map(reader);
						}

						reader.Close();
					}
				}
				catch (SqlException ex)
				{
					throw ex;
				}
				catch(Exception)
				{
					throw;
				}
			}

            		return (T)obj;
        	}

		///<summary>
		/// Execute reader and create list of provided type using IRowMapper interface
		/// </summary>
		/// Type of object to create
		/// Sql Query
		/// IRowMapper to map provided object
		/// List of provided object type
		public List<T> GetList<T>(string query, IRowMapper<T> rowMapper)
		{
			List list = new List();

			using (SqlConnection Connection = new SqlConnection(ConnectionString))
			{
				try
				{
					Connection.Open();

					using (SqlCommand command = Connection.CreateCommand())
					{
						command.CommandText = query;

						SqlDataReader reader = command.ExecuteReader();

						while (reader.Read())
						{
							list.Add((T)rowMapper.Map(reader));
						}

						reader.Close();
					}
				}
				catch (SqlException ex)
				{
					throw ex;
				}
				catch(Exception)
				{
					throw;
				}
			}

			return list;
		}
	}
}

U klasi Connector nema ništa komplikovano. To je ConnectionString koji nam svakako treba, koristimo DataReader jer je neuporedivo brži od popunjavanja npr. DataSet-ova, i mapiramo dati rezultat na neki RowMapper.
Ovde vidimo dve metode, jednu koja vraća jednu instancu datog tipa, i drugu koja vraca listu datih tipova.

public T Get<T>(string query, IRowMapper<T> rowMapper)
public List<T> GetList<T>(string query, IRowMapper<T> rowMapper)

Praktično, prva metoda će “direktno” vratiti zadati tip T, dok će druga vratiti listu tipa T, šta nam više treba u životu?

A evo i kako.

Podrazumevam malo lepše pisanje upita, ali za potrebe primera, biće sasvim dovoljni.

using System.Collections.Generic;
using GHC.Models;
using GHC.RowMappers;

namespace GHC.Services
{
    public class CityService
    {
        private Connector Connector
        {
            get
            {
                // Kreiranje Connector klase cete izdvojiti negde, naravno, ispisao sam ga ovde, tek toliko da imamo neki kompletan kod
                Connector connector = new Connector();
                connector.Server = "server";
                connector.Database = "database";
                connector.Username = "username";
                connector.Password = "password";

                return connector;
            }
        }

        public City GetCity()
        {
            return Connector.Get<City>("SELECT ID, Name, PostalCode FROM City WHERE CityID='00000000-0000-0000-0000-000000000001'", new CityRowMapper());
        }

        public List<City> GetCities()
        {
            return Connector.GetList<City>("SELECT ID, Name, PostalCode FROM City", new CityRowMapper());
        }
    }
}

I eto, sad možete da zaboravite na nepotrebna prepakivanja SQL result set-ova na modele gde zatrebaju.
Sa ovako kreiranim RowMapper-om za model, lista ili objekat je kasnije popunjen u jednoj liniji koda.

WPF – prevod aplikacije

Ovde smo da rešimo dilemu oko prevoda WPF aplikacije.

Tu su delovi koda i objašnjenja, nadam se da će nekome koristiti. Trudiću se da sve ispričam tako da bude razumljivo, obećavam.

Na internetu se dosta pominje prevod WPF aplikacije koristeći “Resources” fajlove, ali to nije ono što sam hteo.
Potreba je bila da postoje ekternalizovani fajlovi sa prevodima, tako da se lako dodaje novi i menja postojeći prevod.
Odluka je da za ovu aplikaciju koristimo XML format. Jednostavan i direktan, lak za pregled i izmenu.

1. XML fajl sa prevodom

Dakle, prvo što će nam trebati je XML fajl sa prevodom, snimite ga u neki folder gde ce ga aplikacija pronaći. Ja nekako volim da koristim “Lang” folder, deluje mi logično…
Evo ga primer XML-a, skraćena verzija radi lakšeg razumevanja.

<?xml version="1.0" encoding="utf-8"?>
<Language name="Srpski" code="sr">
  <Buttons>
    <Ok Text="Ok"/>
    <Save Text="Snimi"/>
    <Cancel Text="Odustani"/>
    <Close Text="Zatvori"/>
    <New Text="Novi" />
    <Edit Text="Izmeni" />
    <Delete Text="Obriši" />
    <Refresh Text="Osveži" />
    <Print Text="Štampa" />
  </Buttons>
</Language>

Vrlo jednostavno, priznaćete.
Sve one koje mrzi da koriste XML, format prevoda može biti bilo kakav, s’tim da ćete u tom slučaju morati da pišete sopstveni parser tog fajla. Ne komplikovati stvari bez preke potrebe.
Ja sam za neke ranije aplikacije koristio INI format, sto se pokazalo kao zaista isplativo, ali sam se ovde ipak odlučio za XML. Zašto, ne sećam se tačno… Ima tu nekih razloga, al nije tema ove priče…

2. Čitanje fajla sa prevodom

Sledeće što će Vam trebati je neki “parser” prethodno kreiranog XML-a, tj. klasa koja će znati da prepozna “ključeve” i vrati vrednost zadatog “ključa”.

Kreirajte novu klasu i dajte joj naziv TranslationService.

Evo ga kod:

using System;
using System.Xml;
using System.IO;

namespace GHC.Utilities
{
    public class TranslationService
    {
        private static string _language = "sr";
        public static string Language { get { return _language; } set { _language = value; } }

        private static XmlDocument doc;
        private static XmlDocument Document
        {
            get
            {
                if (doc == null)
                {
                    doc = new XmlDocument();

                    string langPath = Path.Combine("Lang", Language + ".flgx");
                    if (File.Exists(langPath))
                    {
                        doc.Load(langPath);
                    }
                }

                return doc;
            }
        }

        public static String GetTranslation(string key)
        {
            try
            {
                string xpath = "Language/" + key.Replace(".", "/");
                XmlNode nod = Document.SelectSingleNode(xpath);
                if (nod != null)
                {
                    return nod.Attributes[0].Value.Replace(@"\n", Environment.NewLine);
                }
                else
                {
                    //Ako nije pronadjen zadati kljuc, vrati isti da vidimo sta fali od prevoda
                    return key;
                }
            }
            catch
            {
                //Necemo da dozvolimo da aplikacija pukne ako nesto nije u redu, ili hocemo, vas izbor
                return key;
            }
        }
    }
}

Podrazumevani jezik je Srpski. Ukoliko želite da koristite neki drugi prevod/fajl, promenite “Language” property pre prvog poziva GetTranslation(string key) metode, i eto ga!
Zašto pre prvog poziva? Ako pogledate kod, videćete da se XmlDocument kreira jednom, prvi put kada se pozove, sa zadatim Language-om… Da usput malo razmišljamo i o optimizaciji, jel tako…

Dakle, sva pamet ove klase je u traženju xml nod-a po zadatoj putanji.

string xpath = "Language/" + key.Replace(".", "/");
XmlNode nod = Document.SelectSingleNode(xpath);

Odakle sad ova tačka i kakav je sad ovo string.Replace(“.”, “/”)? Objasniću na kraju, deluje elegantno kasnije u XAML-u.

Sve što ćete trebati da uradite je da pošaljete željenu putanju do ključa u samom XAML-u, dolazimo do toga.

3. XAML – MarkupExtension

MarkupExtension je veoma zanimljiv, ako ga niste koristili do sada, svideće Vam se, sigurno, bar meni jeste. To je ono sto ustvari daje mogućnost da svoje transformacije zadržite na nivou XAML-a, bez preteranih egzibicija u samom kodu.

Negde u kod dodajte novu klasu, mi smo je nazvali TranslationExtension koja nasledjuje baznu MarkupExtension klasu.

using System;
using System.Windows.Markup;

namespace GHC.UI.Extensions
{
    public class TranslationExtension : MarkupExtension
    {
        private string _key;

        public TranslationExtension() { }
        public TranslationExtension(string key) {
            this._key = key;
        }

        [ConstructorArgument("Key")]
        public string Key { get { return _key; } set { _key = value; } }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return GHC.Utilities.TranslationService.GetTranslation(Key);
        }
    }
}

Ne bih da mnogo širim priču dalje oko MarkupExtension-a, sve će biti jasno kada se vidi primer XAML-a

4. Konačno – XAML

I to je to, samo je ostalo da “pozovemo” naš servis za prevod iz XAML-a.

Prvo što nam treba je xmlns našeg servisa. Negde u Window tag dodajte ovo

xmlns:ext="clr-namespace:GHC.Extensions;"

Ovim smo dodali namespace gde se nalazi TranslationService u klasu, slično kao kada bismo koristili “using GHC.Extensions;” u kodu.

Na primer:


 <button name="btnOk"></button>
 <button name="btnCancel"></button>

I to je to!

Sada je jasna ona priča odakle tačka u parseru od malopre. TranslationService menja tačku sa slash-om da bi dobio ispravan xpath, odakle povlači vrednost noda, i vraća prevedeni tekst.

Sve što treba da uradite je da dodate poziv na Translation iz svog XAML-a, i prevod je tu.

Content="{ext:Translation Buttons.Save}"

Nadam se da sam bio od pomoći.

Hello world!

Nadamo se da ćemo na ovom blogu otići korak dalje od Hello World rešenja.

Trudićemo se da podelimo sa Vama neke stvari koje su nama uzele manje ili više vremena, tako da Vi ne morate da gubite to vreme.

Hvala što ste tu, nadamo se da ćete naći nešto korisno.