Tijd voor een afspraak: geen dubbel werk meer…

Deze week was het weer tijd om een afspraak te maken voor het wisselen van
de winterbanden. Waar ik voorheen moest gaan bellen, kon dat nu met een website.
Erg handig, met een paar muisklikken was ik klaar en had ik via keuzelijstjes en
een kalender de afspraak gemaakt. Maar toch zat ik achteraf met een vreemd
gevoel naar de invuloefening te kijken. Ik kreeg dan wel enkele uren later via
de mail de bevestiging binnen maar toch had ik het gevoel dat zo’n schijnbaar
simpele site net de finesses miste om de gebruiker succesvol te begeleiden bij
het maken van de afspraak. En zo’n mailtje verdwijnt op de digitale jachtvelden.

Wat mij namelijk opviel was het gebrek aan integratie… Als ik een afspraak maak dan doe ik dat altijd via mijn Outlook. Eventueel maak ik die via mijn PDA, maar uiteindelijk wordt die dan wel weer gesynchroniseerd. Dus nu zat ik met een afspraak die ik via copy-paste in mijn Outlook moest gaan opnemen. Terwijl er wel degelijk een simpele oplossing is.

bierkrat als kalender

Een eenvoudige oplossing is het aanbieden van een VCalendar. Dit is een klein
(ASCII) bestandje welke op een webpagina aangeboden wordt. Door dit bestandje te openen zal Outlook de inhoud uitlezen en hier automatisch een afspraak uit genereren.

De inhoud moet er zo uit zien (met als bestands-extensie .VCS):

BEGIN:VCALENDAR
PRODID::-//ACME//NONSGML Demo product//EN
BEGIN:VEVENT
CATEGORIES:een
DTSTART:20091021T220406Z
DTEND:20091022T000406Z
LOCATION;ENCODING=QUOTED-PRINTABLE:Demo lokation
DESCRIPTION;ENCODING=QUOTED-PRINTABLE:Demo description
SUMMARY;ENCODING=QUOTED-PRINTABLE:Demo summary
END:VEVENT
END:VCALENDAR

Eventueel is er nog een alternatief (bestands-extensie .ICS) met (iets) meer
mogelijkheden:

BEGIN:VCALENDAR
PRODID:-//Microsoft Corporation//Outlook 12.0
MIMEDIR//EN
VERSION:2.0
METHOD:PUBLISH
X-MS-OLK-FORCEINSPECTOROPEN:TRUE
BEGIN:VEVENT
CATEGORIES:een
CLASS:PUBLIC
CREATED:20091021T220443Z
DESCRIPTION:Demo description
DTEND:20091022T000400Z
DTSTAMP:20091021T220400Z
DTSTART:20091021T220400Z
LAST-MODIFIED:20091021T220443Z
LOCATION:Demo lokation
PRIORITY:5
SEQUENCE:0
SUMMARY:Demo summary
TRANSP:OPAQUE
UID:040000008200E00074C5B7101A82E00800000000D0CD0A42AB52CA01000000000000000
    0100000008FB7CD96FA86DD4B9F5984CF3DA80547
X-ALT-DESC;FMTTYPE=text/html:<!DOCTYPE HTML
PUBLIC “-//W3C//DTD HTML 3.2//EN”>\n<HTML>\n<HEAD>\n<META
NAME=”Generator” CONTENT=”MS Exchange Server version
08.00.0681.000″>\n<TITLE></TITLE>\n</HEAD>\n<BODY>\n<!–
Converted from text/plain format
–>\n\n<P><FONT SIZE=2>Demo
description</FONT>\n</P>
    \n\n</BODY>\n</HTML>
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
X-MICROSOFT-CDO-IMPORTANCE:1
END:VEVENT
END:VCALENDAR

Om het genereren van een VCS mogelijk te maken is eigenlijk niet zoveel
nodig. In onderstaand voorbeeld is de response van de pagina veranderd zodat bij een buttonclick niet de ASPX teruggegeven wordt maar het bewuste ASCII
bestand.

public partial class _Default : System.Web.UI.Page
{
  protected void Button1_Click(object sender, EventArgs e)
  {
    DateTime beginDate = DateTime.Now;
    DateTime endDate = beginDate.AddHours(2);
    string prodIdOrganisation = "ACME";
    string prodIdProduct = "Demo product";

    //Outlook 2007 does not understand a semicolon for multiple categories?
    string categories = "een";
    string location = "Demo lokation";
    string summary = "Demo summary";
    string description = "Demo description";

    PresentVCalendarAppointment(
       Response, prodIdOrganisation, prodIdProduct, beginDate,
       endDate, summary, location, categories, description);
  }

  /// <summary>
  /// Makes the Vcalendar Appointment.
  ///
  /// Warning the Aspx Response is temporary replaced by a vcalender stream
  ///
  /// More info at: http://www.imc.org/pdi/
  ///              and http://www.imc.org/pdi/vcal-10.txt
  /// </summary>
  /// <param name="response">The response.</param>
  /// <param name="prodIdOrganisation">The prodid organisation.</param>
  /// <param name="prodIdProduct">The prodid product.</param>
  /// <param name="beginDate">The begin date.</param>
  /// <param name="endDate">The end date.</param>
  /// <param name="summary">The summary.</param>
  /// <param name="location">The location.</param>
  /// <param name="categories">The categories.</param>
  /// <param name="description">The description.</param>
  public static void PresentVCalendarAppointment(
    HttpResponse response, string prodIdOrganisation,
    string prodIdProduct, DateTime beginDate, DateTime endDate,
    string summary, string location, string categories, string description)
  {
    var streamWriter = new StreamWriter(new MemoryStream());
    streamWriter.AutoFlush = true;
    streamWriter.WriteLine("BEGIN:VCALENDAR");
    streamWriter.WriteLine("PRODID::-//" + prodIdOrganisation +
      "//NONSGML " + prodIdProduct + "//EN");
    streamWriter.WriteLine("BEGIN:VEVENT");
    streamWriter.WriteLine("CATEGORIES:" + categories);
    streamWriter.WriteLine("DTSTART:" +
      beginDate.ToUniversalTime().ToString(@"yyyyMMdd\THHmmss\Z"));
    streamWriter.WriteLine("DTEND:" +
      endDate.ToUniversalTime().ToString(@"yyyyMMdd\THHmmss\Z"));
    streamWriter.WriteLine("LOCATION;ENCODING=QUOTED-PRINTABLE:" +
      location);
    streamWriter.WriteLine("DESCRIPTION;ENCODING=QUOTED-PRINTABLE:" +
      description);
    streamWriter.WriteLine("SUMMARY;ENCODING=QUOTED-PRINTABLE:" + summary);
    streamWriter.WriteLine("END:VEVENT");
    streamWriter.WriteLine("END:VCALENDAR");

    response.Clear();
    response.AppendHeader( "Content-Disposition",
      "attachment;filename=Appointment.vcs");
    response.AppendHeader("Content-Length",
      streamWriter.BaseStream.Length.ToString());
    response.ContentType = "application/download";
    response.BinaryWrite((streamWriter.BaseStream as MemoryStream).
      ToArray());
    response.End();
  }
}

Dus dit kan de uitkomst voor mijn ‘luxe probleem’ zijn.  Het maakt een
website eenvoudig compleet door ook aandacht te schenken aan de laatste schakel van de afspraak, namelijk dat ik die niet vergeet.

Meer info: http://www.imc.org/pdi/

Isolated Storage uit de isolatie

Momenteel werk ik mee aan een project waarbij een Excel 2007 VSTO AddIn
gecombineerd wordt met een Excel 2007 UDF (User Defined Functions) AddIn. Beiden worden bij het starten van Excel actief en samen beiden ze een krachtige OBA (Office Business Application) oplossing.

Maar we liepen tegen een beperking op. Beiden AddIn’s willen logica delen
rond toegang tot een webservice waarvoor de gebruiker moet inloggen.  Helaas
voor ons weten de addIns niks van elkaars bestaan terwijl het is voor de
gebruiker wel zo praktisch om maar één keer in te loggen.

Dwangbuis

De oplossing voor ons probleem was het gebruik van Isolated Storage (wat mij
betreft staat deze term hoog in de top 10 van wazige .Net omschrijvingen, net
iets achter ‘obfuscate’). Hierbij wordt de mogelijkheid geboden om specifieke
data op te slaan en uit te lezen waarbij de toegang van de data bv. slechts
beperkt wordt tot de gebruiker zelf. Dit kan ook gecombineerd worden tot het
beperken van de toegang tot slechts enkele applicaties (lees assemblies).

Het gebruik van Isolated storage voor het delen van informatie tussen twee
toepassingen begin met het bouwen van een library project voor gezamenlijk
gebruik. Deze moet gesigned zijn. Creëer een class met daarin de logica om te
lezen en te schrijven naar een Isolated Storage stream.

Classdiagram van de Isolated Storage helper

Hier volgt een voorbeeld met twee console applicaties die samen informatie
delen:

public class IsolatedStorager
{
  private static string _FileName = "IsolatedStorageDemo.dat";

  private static IsolatedStorageFile _IsolatedStorageFileStore =
     IsolatedStorageFile.GetStore(
       IsolatedStorageScope.User |
          IsolatedStorageScope.Assembly
       , null
       , null);

  public static void WriteToStorage(string name, string password)
  {
    IsolatedStorageFileStream
      isolatedStorageFileStream =
        new IsolatedStorageFileStream(
          _FileName
          , FileMode.Create
          , _IsolatedStorageFileStore);

    StreamWriter writeStream = new
      StreamWriter(isolatedStorageFileStream);

    writeStream.WriteLine(name);
    writeStream.WriteLine(password);
    writeStream.Flush();
    writeStream.Close();
    isolatedStorageFileStream.Close();

    _IsolatedStorageFileStore.Close();
  }

  public static void ReadFromStorage(out string name, out string password)
  {
     IsolatedStorageFileStream
        securityDataFile =
           new IsolatedStorageFileStream(
              _FileName
              , FileMode.Open
              , _IsolatedStorageFileStore);

     StreamReader readStream = new StreamReader(securityDataFile);
     name = readStream.ReadLine();
     password = readStream.ReadLine();

     readStream.Close();
     securityDataFile.Close();
     _IsolatedStorageFileStore.Close();
  }

  public static void Delete()
  {
    using (_IsolatedStorageFileStore)
    {
      if (_IsolatedStorageFileStore.GetFileNames(_FileName).Length > 0)
      {
        _IsolatedStorageFileStore.DeleteFile(_FileName);
      }
    }
  }

  public static bool Exists()
  {
    using (_IsolatedStorageFileStore)
    {
      return _IsolatedStorageFileStore.GetFileNames(_FileName).Length == 1;
    }
  }
}

Zoals je ziet is het nu mogelijk om heel basaal een naam en wachtwoord te
lezen en te schrijven.

Schrijven gaat als volgt:

class Program
{
  static void Main(string[] args)
  {
    IsolatedStorager.WriteToStorage("DemoUser", "DemoPassword");
    Console.WriteLine("Text is written");
    Console.ReadKey();
  }
}

En teruglezen ziet er zo uit:

class Program
{
  static void Main(string[] args)
  {
    //IsolatedStorager.Delete();
    if (IsolatedStorager.Exists())
    {
      string name;
      string password;

      IsolatedStorager.ReadFromStorage(out name, out password);
      Console.WriteLine("User: " + name);
      Console.WriteLine("Password: " + password);
    }
    else
    {
      Console.WriteLine("No storage found");
    }

    Console.WriteLine("Ready...");
    Console.ReadKey();
  }
}

En zie daar, de twee console applicaties delen informatie. Maar waar blijft
die informatie? Door simpel op IsolatedStorageDemo.dat te zoeken kom ik uit
C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin. Daar staat het bestand en de inhoud is gewoon in ascii uit te lezen. Nu is dit op zich geen probleem, de
inhoud is alleen voor deze gebruiker toegankelijk, maar informatie die zelfs
niet voor de gebruiker leesbaar mag zijn moet nog verder beveiligd worden met
encryptie.

Voor administrators (en gebruikers) is het mogelijk om meer informatie te
verkrijgen over de opslag:

C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin>storeadm.exe /list

Microsoft (R) .NET Framework Store Admin 3.5.21022.8

Copyright (c) Microsoft Corporation.  All rights reserved.

Record #1

[Assembly]

<StrongName version=”1″

Key=”[knip]”

Name=”IsClassLibrary”

Version=”1.0.0.0″/>

Size : 2048

Het is zelfs mogelijk om alle opslag weer te verwijderen of de opslag te
beperken.  Ga er dus niet klakkeloos van uit dat een eenmaal aangemaakte
Isolated Storage onbeperkt aanwezig zal zijn.

Voor meer informatie:

Introduction to Isolated Storage

Performing Isolated Storage Tasks

Isolated Storage Tool (Storeadm.exe)