Archive for tag: C

CSS rotation af indhold

Nogle gange har man brug for at rotere indhold for at få en speciel effekt. Dette kan med CSS3 gøres ret let, hvis ellers browseren fatter CSS3.

Her rotetes et element 45 grader med uret:

.label {
    -webkit-transform: rotate(45deg);
    -moz-transform: rotate(45deg);
    -ms-transform: rotate(45deg);
    -o-transform: rotate(45deg);
    filter: progid:DXImageTransform.Microsoft
           .Matrix(m11=0.71,m12=0.71,m21=0.71,m22=0.71);
}

NB: linjeskiftet er for at undgå koden flyder over i siden, det linjeskift skal fjernes, hvis du klipper koden ud og bruger i dit eget stylesheet.

Bemærk filter-reglen, som retter sig mod IE i tidligere versioner. Her er beregningen som følger:

m11 = sin (45 * pi / 180)
m12 = cos (45 * pi / 180)
m21 = cos (45 * pi / 180)
m22 = sin (45 * pi / 180)

Entity Framework Code First Migrations på en Umbraco installation

Arbejder du med kode til en Umbraco installation, som anvender Entity Framework 6 og Code First Migrations, kan det være du kommer i samme situation som mig, hvor Migrations ikke bliver kørt ifm. Web Deploy.

Jeg kunne ikke umiddelbart finde frem til årsagen, men jeg fandt en vej udenom problemet. Det er ikke en perfekt løsning, da den involverer noget manuelt arbejde, men nogle vil påstå, at det er en bedre metode i et produktionssetup, hvor du gerne vil have 100% kontrol over, hvad der sker med din database og hvornår det sker.

Til formålet anvender jeg Package Manager Console, hvor jeg skyder en update-database af mod min produktionsdatabase server og får et script genereret, som jeg så kan fyre af i MSSMS (mod min produktionsdatabase server).

Inden jeg kunne gøre dette, løb jeg dog ind i et par problemer. Det første var at DbProviderFactory skal være til stede i min web.config, for ellers ville update-database kommandoen ikke køre. Dette betyder helt konkret, at flg. skal tilføjes i system.web-sektionen af web.config:

  <system.data>
    <DbProviderFactories>
      <remove invariant="System.Data.SqlClient" />
      <add name="SqlClient Data Provider"
           invariant="System.Data.SqlClient"
           description=".Net Framework Data Provider for SqlServer"
           type="System.Data.SqlClient.SqlClientFactory, System.Data, 
                 Version=2.0.0.0, Culture=neutral, 
                 PublicKeyToken=b77a5c561934e089" />
    </DbProviderFactories>
  </system.data>

Når dette er på plads kan update-kommandoen udføres med flg. format:

update-database -script -projectname <projektet med entity datacontext>
     -connectionstring <connectionstring til produktionsdatabasen>

NB: Det er naturligvis en forudsætning, at du har adgang til din produktionsdatabase server fra din udviklermaskine (hvilket nok i større setups ikke er sandsynligt), da update-database skal læse migrationshistorikken fra databasen, for at vide, hvilke migrationer der skal udføres.

Efter denne kommando er udført, får du, i Visual Studio, et vindue med SQL-koden, som modsvarer migrationen. Denne kode kan så udføres i MSSMS mod produktionsdatabasen og migrationen er dermed gennemført.

Hvis du føler dig rigtig modig, kan du undlade -script parameteren og få udført opdateringen direkte fra PMC'en. Det sparer dig for at skulle udføre scriptet i MSSMS efter det er genereret i PMC, men det fjerner også muligheden for, at kontrollere, hvad migrationsscriptet rent faktisk har tænkt sig, at gøre ved din produktionsdatabase...

Hvordan laver man geolokation

Jeg står overfor at skulle arbejde med geolokation på en hjemmeside, nærmere bestemt finde ud af hvormange lokationer der findes indenfor en radius af et givet punkt.

Det har jeg ikke arbejdet med før, men jeg fornemmer det er lettere end det har været, da der dels er nogle gode resurser omkring emner, dels er nogle API'er man kan anvende til udføre noget af arbejdet.

Opgaven er såmænd ret enkel, nemlig "find alle, i en database registrerede, lokationer, som er indenfor en radius af X km fra min aktuelle lokation".

Det involverer umiddelbart flg.

  1. at jeg kender eller kan finde den aktuelle lokation
  2. at jeg kender lokationen på alle de ting der skal findes
  3. at jeg, på en nem måde, kan foretage et opslag på disse oplysninger

Find aktuelle brugers lokation

Det første kan jeg i nogen udstrækning bruge browserens faciliteter til. Nyere browsere har indbygget en eller anden grad af lokationsbestemmelse og den tager jeg udgangspunkt i.

Det kunne f.eks. se således ud:

window.navigator.geolocation.getCurrentPosition(function(pos) { 
    var lat = pos.coords.latitude;
    var lon = pos.coords.longitude;
})

Find lokation på adresser

Det andet kan jeg (nok) bruge Google's Geocoding API til. Det skal bruges på den måde, at jeg vha. en adresse omsætter denne til et sæt længde- og breddegrader. Disse gemmer jeg  sammen med det der skal findes.

Dette API kræver, at jeg opretter en API-nøgle før jeg kan foretage opslag (se instruktionerne på siden til Geocoding API'et), men når det så er gjort, kan jeg spørge på en adresse med et kald til flg. url:

var url = "https://maps.googleapis.com/maps/api/geocode/json?"
    + "address=Hovedgaden+12,8000+Århus&key=[API-nøglem]";

Udtræk lokationer der matcher området

Det tredje kan jeg, med udgangspunkt i jeg anvender SQL Server, gøre ved et fancy opslag, hvor jeg får returneret et antal rækker, som ligger indenfor et givet cirkulært område. Hvordan der foretages en sortering efter nærhed, har jeg ikke gennemskuet, men med noget dataanalyse kan man måske nå frem til et approximeret sorteret resultat - måske endda et reelt sorteret resultat. Hvem ved...? Mit umiddelbare behov er dog ikke, at resultatet er sorteret efter nærhed, så det spekulerer jeg ikke så meget i lige nu.

Anyway! For at udtrække de lokationer, som ligger indenfor en given radius (på grundlag af længde- og breddegrader), kan jeg så bruge denne SQL-konstruktion:

declare 
   @latStartingPoint float, 
   @lngStartingPoint float, 
   @distKmFromStartingPoint float;

set @latStartingPoint = 55.0
set @lngStartingPoint = 9.0
set @distKmFromStartingPoint = 13.0

select * 
from testLocation 
where ( 
   6371 * 
   acos( 
       cos( radians(@latStartingPoint) ) * 
       cos( radians( lat ) ) * 
       cos( radians( lng ) - radians(@lngStartingPoint) ) + 
       sin( radians(@latStartingPoint) ) * 
       sin( radians( lat ) ) 
   )) < @distKmFromStartingPoint

Jeg har erklæret 3 variable i SQL'en ovenfor, hvor

  • @latFromStartingPoint er breddegraden for udgangspunktet
  • @lngFromStartingPoint er længdegraden for udgangspunkt
  • @distKmFromStartingPoint er antal km fra center der ønskes resultater fra
  • Det magiske tal 6371, er jordens radius

Konklusion

Som det kan ses, er det i teorien, rimeligt ligetil... så må vi se om det også er i praksis :-)

 

Datastyrede routes i ASP.NET MVC

I disse tider med større og større fokus på SEO-venlighed, kan det være rart, hvis man kan basere sine url'er på data fra eksempelvis sin database.

Man kunne have en produktdatabase, hvor produktet har en unik (url-venlig) streng, som kan anvendes til at finde produktet. I et setup uden nogen specielle regler, kan dette være en udfordring at få gjort stømlinet. Med en routedefinition som denne:

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { 
        controller = "Home", 
        action = "Index", 
        id = UrlParameter.Optional 
    }
);

hvor produkt-controlleren hedder ProduktController, action-metoden til at vise et produkt hedder Vis og produktets urlnavn ligger i id-parameteren, kunne url'en til et produkt se således ud:

/produkt/vis/stor-tallerken-med-sort-kant

Det kan være fint nok ift. SEO, men det ville jo være rart, hvis man kunne slippe for de lidt overflødige controller- og action-tekster, så url'en kom til at se således ud i stedet:

/stor-tallerken-med-sort-kant

Dette kan opnåes med en RouteConstraint og en wildcard-urlparameter. Route-mapping kunne derfor se således ud:

routes.MapRoute(
    name: "Default",
    url: "{id}",
    defaults: new { controller = "Produkt", action = "Vis" },
    constraints: new { id = new ProduktRouteConstraint() }
);

Nu kaldes ProduktRouteConstraint's Match-metode hver gang routing kaldes. Denne Match-metode skal så håndtere et opslag i produktdatabasen for at se, om den angivne id refererer til et eksisterende produkt. Match-metoden returnerer en bool, der indikerer om den aktuelle route skal anvendes eller ej, dvs. om produktet findes i dette tilfælde. En implementering kunne se således ud:

public class ProduktRouteConstraint : IRouteConstraint
{
    public bool Match(
            HttpContextBase httpContext,
            Route route,
            string parameterName,
            RouteValueDictionary values,
            RouteDirection routeDirection
        )
    {
        var svc = new ProduktService();
        var id = values["id"].ToString();
        return svc.FindesProduktMedUrlnavn(id);
    }
}

Match-metoden fyrer op under en produktservice-klasse, der, via metoden FindesProduktMedUrlnavn, kan svare på, om produktet med det angivne navn (fra url'en) findes eller ej.

Hvis url'en ikke er et match, fortsætter routing-systemet videre til næste regel (som det altid har gjort). Derfor skal denne regel helst ikke være den sidste regel i rækken af routing-regler. 

En ting man skal være opmærksom på med denne metode er, at routing-systemet aktiveres hver gang der foretages et request til sitet. Derfor skal routingconstraints helst være så optimerede som muligt, da sitet ellers vil virke langsomt. Det vil med andre ord være en god idé, at overveje caching af grundlaget for disse specielle routingregler, i dette eksempel produktets urlnavne.

CSV parser til .net projekter

Det sker at man falder over en opgave, som kan løses ved at importere data fra en CSV-fil (kommaseparerede data) og i den forbindelse kunne man jo strikke sin egen parser sammen.

CSV-formatet er dog lidt mere tricky end man lige umiddelbart skulle forestille sig, så derfor bør man nok kigge på markedet, om der ikke findes en anden som allerede har strikket noget sammen (og som er testet grundigt igennem af mange andre).

Det var netop hvad jeg gjorde. Jeg fandt frem til denne på CodeProject: http://www.codeproject.com/Articles/9258/A-Fast-CSV-Reader

Den ser ud til at fungere fint og kan installeres i et .NET-projekt vha. NuGet, så det bliver vist ikke ret meget lettere at komme igang. Oveni det er den ret simpel at anvende.

C# out og ref-parametre

Har man brug for at få værdier ud af en funktion kan man naturligvis bruge funktionens returværdi, hvilket er oplagt i de fleste tilfælde. Der kan dog være tilfælde, hvor man har brug for at returnere flere værdier fra en funktion og i disse tilfælde har man et par yderligere muligheder.

out-parametre

Med out-parameteren kan man sende en værdi ud af et funktionskald. Parameteren SKAL tildeles en værdi inde i funktionen, ellers får man en compilefejl. Parameteren er KUN til output, evt. værdi i input vil blive ignoreret. Følgende vil give en compilefejl (fordi p endnu ikke er tildelt en værdi inde i fn, inden den refereres):

void fn(out int p)
{
    return p + 1;
}
var x = 1;
fn(out x);

Samtidig opfattes denne parameter alene som en output-parameter, dvs. den værdi parameteren evt. måtte have i kaldet, kan ikke bruges i funktionen. Følgende vil f.eks. give en kørselsfejl, fordi parameteren p ikke er tildelt en værdi inden den bruges (inde i funktionen):

void fn(out int p)
{
    Console.WriteLine(p);
    p = 2;
}
var x = 10;
fn(out x);

ref-parametre

Med ref-parametre kan man sende værdier ind i en funktion og samtidig give funktionen mulighed for at ændre værdien af den parametere (så den også ændres i den kaldende parts variabel). Dette kunne se således ud:

void fn(ref int p)
{
    p++;
}
var x = 1;
fn(ref x);
// x er nu 2;

Denne måde at bruge ref på, kan være "farlig" og betragtes af mange som dårlig kodestil. Begrundelsen er, at kaldet til fn har en skjult sideeffekt på den kaldende parts kode. Denne sideeffekt er svær at forudse, hvis man ikke lige har adgang til koden i fn og kan se hvad der sker. Selvom man havde adgang til koden, var det ikke nødvendigvis let at gennemskue denne sideeffekt alligevel. Det er derfor ikke tilrådeligt, at benytte ref på denne måde.

Når ref-modifikatoren bruges er det den variabel som står på parameterens plads i kaldet, der ændres (direkte i den hukommelsesplads variablen ligger). Det betyder også, at når der medsendes en referencevariabel (bla. objekter, strenge, arrays), så er det referencen der ændres, dvs. der peges på en anden forkomst af den medsendte type. Dette er fordi referencevariable i sig selv er referencer (navnet havde nok allerede afsløret dette) og det er altså pegepinden der ændres.

Det samme gælder for simple typer, hvor værdien ændres direkte i den hukommelsesposition, hvor variablen er defineret inden kaldet til funktionen.

Hukommelsespositionen kan dels være på "heapen", dels på stakken. Stakken benyttes hvis kaldet sker med en lokal variabel fra en anden funktion.

Umbraco pre v4 database oprydning

Jeg havde for nylig et site der kørte på Umbraco og som kørte enormt langsomt og fungerede egentlig ikke rigtigt. Jeg undrede mig en del over det, for indholdsmæssigt var det ikke et tungt site (kun få sider - mindre end 100), men et site der havde været i drift i en længere periode. Jeg besluttede mig for at finde ud af hvorfor det ikke fungerede. Her er hvad jeg fandt frem til...

Symptomerne

Sitet kørte som nævnt ikke rigtigt. Helt konkret var det enormt langsomt til at indlæse træstrukturen og det var et held, hvis man fik lov til at oprette nye dokumenter.

Det er klart, at man ikke kan forvente hvad som helst af en 10+ år gammel undskyldning for en server, men jeg havde da også andre Umbraco-sites på denne server, som slet ikke havde problemer med at køre, så jeg mente jo nok dette ikke burde være et problem. Også det faktum, at der var tale om et Umbraco v3.0.6 kunne vække en vis bekymring, men igen, så var der også andre sites i denne version, på samme server, som kørte tilfredsstillende.

Opdagelsen

Efter af have kigget lidt rundt i Umbraco-admin, uden dog at kunne finde noget, der virkede himmelråbende tåbeligt konfigureret, bedrog jeg mig ud i stifinderen, for at se om der kunne være noget der. Første stop web.config, men også her så tingene tilforladelige ud, så heller ikke der synes jeg, der var noget at hente.

Efter lidt stikken og justeren, fandt jeg frem til databasefilerne (data og log) for sitet og til min store overraskelse opdagede jeg, at data-filen fyldte 2.3+ Gb! Det var vist ikke nødvendigt for sådan et lille site, så noget måtte være galt, men hvad!?

Undersøgelsen

Gad vide hvordan sådan en Umbraco database er bygget op? En hel masse tabeller præfikset med cms og umbraco og lidt svær at gennemskue, hvis man ikke har arbejdet indgående med sådan en før (hvilket jeg ikke havde), så Google måtte kunne hjælpe...(?)

Først og fremmest måtte jeg have et overblik over hvormeget data der var i hvilke tabeller. Til dette formål fandt jeg flg. SQL-script, som, om en given database, kunne fortælle mig, hvormange rækker der er i hver tabel, hvormeget tabellens indhold fylder i Kb m.m.

SELECT 
    t.NAME AS TableName,
    s.Name AS SchemaName,
    p.rows AS RowCounts,
    SUM(a.total_pages) * 8 AS TotalSpaceKB, 
    SUM(a.used_pages) * 8 AS UsedSpaceKB, 
    (SUM(a.total_pages) - SUM(a.used_pages)) * 8 AS UnusedSpaceKB
FROM 
    sys.tables t
INNER JOIN      
    sys.indexes i ON t.OBJECT_ID = i.object_id
INNER JOIN 
    sys.partitions p ON i.object_id = p.OBJECT_ID 
                    AND i.index_id = p.index_id
INNER JOIN 
    sys.allocation_units a ON p.partition_id = a.container_id
LEFT OUTER JOIN 
    sys.schemas s ON t.schema_id = s.schema_id
WHERE 
    t.NAME NOT LIKE 'dt%' 
    AND t.is_ms_shipped = 0
    AND i.OBJECT_ID > 255 
GROUP BY 
    t.Name, s.Name, p.Rows
ORDER BY 
    t.Name

Efter at have kørt dette script i SSMS, fik jeg en fornemmelse af, at problemet havde med versionering af dokumenterne at gøre. De tabeller som indeholdte markant mere data end forventet, var cmsContentVersion, cmsDocument, cmsPropertyData og umbracoLog. Uden at ville (og kunne) gå i yderligere detaljer om, hvad disse tabeller konkret bruges til, indikerer det i det mindste, at det har noget med versionering at gøre. Spørgsmålet er bare, hvorfor? Der er ikke specielt meget aktivitet i relation til vedligeholdelse af indhold på sitet, så hvorfor er der oprettet så mange rækker i disse tabeller (der var over 500.000 i cmsDocument og cmsContentVersion og over 3.500.000 i cmsPropertyData, ligesom der også var omkring 1.200.000 i umbracoLog...!

Et kig i umbracoLog afslører en periodisk publisering af to specifikke dokumenter, som resulterer i 4 nye rækker i umbracoLog, samt nye 2 eller flere rækker i hver af de andre tabeller, hver gang. Hver gang vil sige, hvert minut 24/7/365 - det bliver til en del rækker med tiden... :-)

Det var naturligvis ikke en optimal situation, så der måtte da være en løsning på dette. Hvorfor blev disse to dokumenter (indholdssider på linje med en masse andre på sitet) publiseret med 1 minuts intervaller og mere vigtigt, hvordan fik jeg det så stoppet!?

"Løsningen"

Derfor måtte jeg finde ud af, hvordan jeg kunne løse dette. Google er min ven(!), men selv venner har sine begrænsninger, så det lykkedes den ikke at fremskaffe en løsning. Den eneste tilnærmelsesvis overkommelige løsning var, at opgradere til en nyere version, hvor dette ikke længere var et problem. Det lader nemlig til at være et bug i nogle versioner (herunder v3.0.6, som dette site jo kører), som bevirker, at der publiseres på denne måde.

Jeg skal erkende, at jeg heller ikke har søgt efter en løsning meget mere end en times tid, da sitet skulle pilles ned indenfor en overskuelig tidshorisont, så det kan være der rent faktisk kan køres et workaround, som løser dette, men jeg er som sagt ikke faldet over det.

Det jeg så gør i stedet, er symptombehandling (when all else fails...). Det indbefatter en periodisk udførsel af et SQL script, som sletter alle versioner op til en bestemt dato (i nedenfor viste script, er det til dagen før den dato hvorpå scriptet udføres) og som ikke er den aktive version. Dette script ser således ud.

DECLARE @createdDate Datetime
SET @createdDate = DateAdd(day, -1, getdate())

DELETE FROM cmsPropertyData WHERE
    versionId NOT IN (
              SELECT versionId 
              FROM cmsDocument 
              WHERE updateDate > @createdDate 
                 OR published = 1 
                 OR newest = 1) AND
    contentNodeId IN (SELECT DISTINCT nodeID FROM cmsDocument)

DELETE FROM cmsContentVersion WHERE
    versionId NOT IN (
              SELECT versionId 
              FROM cmsDocument 
              WHERE updateDate > @createdDate 
                 OR published = 1 
                 OR newest = 1) AND
    ContentId  IN (SELECT DISTINCT nodeID FROM cmsDocument)

DELETE FROM cmsDocument WHERE
    versionId NOT IN (
              SELECT versionId 
              FROM cmsDocument 
              WHERE updateDate > @createdDate 
                 OR published = 1 
                 OR newest = 1) AND
    nodeId IN (SELECT DISTINCT nodeID FROM cmsDocument)

Dette script, når det bliver gemt som en fil og placeret på serveren, kan så, med jævne mellemrum, aktiveres, som en baggrundsopgave på serveren, hvor scriptet kaldes med sqlcmd.

sqlcmd -S myServer\instanceName -i C:\sqlscripts\umbracoCleanup.sql

Konklusion

Nu har jeg i det mindste fået lidt styr på databasevæksten på mit site, selvom jeg ikke synes dette er den optimale løsning (langt fra - det bedste ville jo være, at publiseringen ikke gik i selvsving!). Nogle gange er vi bare nød til, at nøjes med mindre, så vi kan komme videre med det der er rigtig fedt! :-)

 

Ikke standard 404 handler i ASP.NET MVC 3

Jeg vil, i tilfælde med indhold som ikke eksisterer, gerne returnere en side med statuskoden 404 (af hensyn til SEO), men samtidig gerne have, at siden ikke er en standard 404-side. Dette sker som udgangspunkt ikke i ASP.NET MVC3, da MVC 3 kobler en standard handler på fejlsider. Dette er også helt fint, men i disse særlige tilfælde, hvor jeg gerne selv vil styre 404-siden, er det træls.

Efter lidt søgen rundt på nettet (primært StackOverflow), fandt jeg, i en kommentar til et indlæg, en henvisning til denne egenskab på Response-objektet:

Response.TrySkipIisCustomErrors = true;

Den sørger åbenbart for, at den standard handler som er opsat, ikke aktiveres, når jeg sætte Response.StatusCode = 404.

Dermed kan jeg med følgende kode i min Controller's Action, få vist en ikke standard 404-side (som, i mit tilfælde, ligger i et view med navnet "NotFound"):

public ActionResult View(int id)
{
   Response.Status = "404 - Not Found";
   Response.StatusCode = 404;
   return View("NotFound");
}

Jeg kunne måske endda pakke denne kode ind, så jeg kan kalde den fra forskellige controllers, som så kunne have deres egne specialiserede views til NotFound-håndtering...

Bundling og minification i ASP.NET MVC 3

Har man arbejdet med ASP.NET MVC 4, ved man der findes en bundling og minification mulighed indbygget i frameworket. Dette er umiddelbart nyt ifht. MVC 4 og fandtes ikke som udgangspunkt i MVC 3.

Man kan dog benytte sig af disse features i et MVC 3-projekt, hvis man benytter .NET 4, man skal bare lige selv sætte de grundlæggende ting op i sit projekt (registrering af bundles, inkludering af System.Web.Optimization og kald af bundles i sit layout).

Du kan finde en nærmere forklaring til dette i artiklen om "Using MVC 4 bundling and minification in an MVC 3 project".

Se også

Denne artikel på MSDN er endnu mere uddybende omkring emnet.

"Javascript minification in MVC 3" kommer også ind på, hvordan man kan overstyre bundling- og minificationprocessen, så man kan smide sine egne regler ind i mix'et.

Hvis du arbejder med webforms kan du måske få lidt ud af kigge på, hvordan man kan benytte scriptmanager til at kombinere scriptfiler.

Azure Emulator og SSL

Jeg arbejder på en webapplikation der skal køre i Azure og jeg har dermed også behov for at kunne teste lokalt.

Mit setup er:

  • Visual Studio 2012
  • ASP.NET MVC 3
  • .NET 4.0
  • Windows 8 Pro x64
  • fuld IIS 8

Denne webapp skal køre en del af sitet over SSL, så derfor har jeg fået sat mit udviklingsmiljø op til at benytte SSL, hvilket umiddelbart også fungerer.

Jeg har dog oplevet lidt problemer med, at uploade filer over SSL. Dette kan nok skyldes mange ting, men umiddelbart udmynter det sig i, at jeg får en 404 tilbage, når jeg POST'er filen til en, 100% sikkert, eksisterende action på min controller. Først troede jeg det havde noget med mine routing at gøre, men GET-request til samme action fungerer jo, så jeg har udelukket denne mulighed.

Jeg har endnu ikke fundet en løsning på dette problem, men jeg har fundet et workaround.

Det jeg gør er, i stedet for at køre over den localhost IP, som IDE'en starter min browser på (https://127.0.0.1/), så kører jeg over den localhost IP som IIS manageren peger på, nemlig https://127.255.0.0:444/

Porten kan dog variere, men hvis man lurer i Output-vinduet af VS, når Emulatoren startes (ifm. build and run), skal man bruge det private portnummer, hvilket kan ses i den linje hvor der står

Windows Azure Tools: Warning: Remapping private port 443 to 446 in role...

Når jeg gør det, lader det til at min action bliver aktiveret korrekt og filen bliver uploadet som den skal.