C# TypeProviders ?!

Last week at the great community conference NRW Conf 2013 in Wuppertal i heard Steffen Forkmann speaking about F# TypeProviders. This was the second time i listened to this topic – sometimes it needs a repetitive consumption of a topic to trigger a reaction.

Excited by the F# TypeProviders i tried to also have TypeProviders in C#.

As an example i have written the Regex TypeProvider in C#.

Unfortunately the C# can not infer dynamically generated types like F# interactive does. So the type information is not available at edit time.

Editor View

The debugger instead can see the dynamically generated type and show the properties.

Debugger View

For full source to try yourself see GitHub:

https://github.com/FrankPfattheicher/CSharpx

I am interested in your opinion about the usefulness of this approach.

Veröffentlicht unter Uncategorized | 1 Kommentar

WPF Application with Current Culture

This is a personal snippet reminder Zwinkerndes Smiley

      FrameworkElement.LanguageProperty.OverrideMetadata(
                      typeof(FrameworkElement),
                      new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));

(untwitterable)

Veröffentlicht unter Uncategorized | Kommentar hinterlassen

Mein Traum vom Open Space Süd 2014

Bereits vier Mal haben wir mit der .NET User Group Karlsruhe den .NET Open Space Süd in Karlsruhe veranstaltet. Wie die letzten Male war es ein ungemein interessantes Ereignis.

Meine Hauptgründe für diese Veranstaltung sind der ungetrübte Blick auf aktuelle Trends und noch mehr der unglaubliche Motivationsschub, den ich mitnehmen kann.

Selbstverständlich freue ich mich jedes mal wieder alte und neue Gesichter zu sehen
“Meet your timeline Zwinkerndes Smiley

Trotzdem gibt es bei dieser Veranstaltung Verbesserungspotential!

Was mich immer so ein Bisschen stört, ist, dass obwohl es der Sinn und Zweck eines Open Space ist, Gespräche unterbrochen werden, weil einige „noch mal schnell ins Hotel” oder “kurz nach Hause” wollen, der Veranstaltungsort für die Party gewechselt wird oder auch einige mit dem angebotenen Kaffee nicht zufrieden sind und sich deshalb anderweitig versorgen – ein kommen und gehen…

Mein Traumvorstellung ist ein kombinierter Ort für die Veranstaltung und die Übernachtung der Gäste. Man kann sich jederzeit für eine kurze Zeit ausklinken, wenn man eine Pause benötigt.

Da ich mit meinem Traum nicht bis zum nächsten Jahr warten konnte hab ich mich gleich auf die Suche gemacht.

Hier ist mein Traum für einen künftigen Open Space

Das Forum Hohenwart für Bildung und Begegnung:

http://www.hohenwart-forum.de/

Hohenwart3

  • Alle Teilnehmer finden hier ein Zimmer für die Zeit der Veranstaltung
  • Es gibt mehr als ausreichend Räume unterschiedlicher Größe
  • Der gesamte Bereich ist mit entsprechender Technik ausgerüstet
  • Neben den Räumen stehen vielfältige Außenbereiche zur Verfügung
  • Auch die Verpflegung ist geregelt
  • Mitten im Grünen für kurze Ausflüge zur Entspannung oder Vertiefung
  • Abseits von Lärm und sonstigen Ablenkungen
  • Die Kosten pro Person sind überschaubar
  • Sponsoren haben die Möglichkeit sich zu präsentieren

Hohenwart1

In dieser Umgebung können stundenlang anregende Gespräche stattfinden.

Hohenwart4

Bis in die späte Nacht…

Hohenwart2

Überall stehen weitere Sitzgruppen zur Verfügung.

Hohenwart5

Leider ist kein Termin mehr für 2014 frei!!!

Ich finde diese “Location” so interessant, dass ich mich um eventuelle Termine für 2015 oder 2016 bemühen werde.

Mein Wunsch an Euch ist eine Rückmeldung, wie Ihr zu einem Open Space in dieser Form steht. Welche Bedenken und Einwände sind da?

Erwartungsvolle Grüße aus Karlsruhe

Frank

Veröffentlicht unter Open Space | Verschlagwortet mit | 6 Kommentare

MongoDB Client-Trigger mit Reactive Extensions (Rx)

Ich bin also nicht so der Datenbanktyp, deshalb habe ich mich beim aktuellen Projekt für NoSQL entschieden. Ich denke man findet nur in einem wirklichen Projekt die Schmerzen einer Technologie. Ich will auch nicht in JavaScript programmieren, also ist meine Software in C# implementiert.

Ziel

Eine meiner ersten Anforderungen war, einen clientseitigen Trigger zu bekommen, der von der Datenbank ausgelöst wird, wenn ein Datensatz geändert wird – kann die Datenbank natürlich nicht so. Damit’s später auch schön zu verwenden ist wollte ich es mit den Reactive Extensions implementieren.

So soll’s verwendet werden:

      var elementsChanged = new MongoTrigger(dbClient)
.Where(trigger => trigger.Context == "MyDatabase.Elements");
      elementsChanged.Subscribe(trigger => MessageBox.Show(trigger.ToString()));

Ich habe eine Datenbank “MyDatabase” mit einer Collection “Elements”. Immer wenn Änderungen in dieser Collection erfolgen soll die angegebene Funktion (hier Lambda mit MessageBox) ausgeführt werden.

Der Vorteil ist die Verwendung der Reactive Extensions mit deren Hilfe ganz Einfach eine Filterung über Linq oder die Ausführung auf einem anderen Thread mit ObserveOn möglich.

Recherche

Nach kurzer suche im Internet findet man den Hinweis, dass alle Änderungen der Datenbank bei replizierten Installationen in der Collection oplog gespeichert werden.

Diese Implementierung erfordert, dass die Datenbank als Replica Set konfiguriert ist! (Mehr zu Replikation)

Der nächste Hinweis ist die Existenz von sogenannten tailablen Cursorn. Alles zusammen ergibt eine Abfrage, die bei neuen Einträgen das nächste Ergebnis liefert. Das ist im Prinzip schon die Anforderung für IObservable!

Da die Abfrage natürlich im MoveNext blockiert bis der nächste Eintrag erzeugt wird ist ein Backgroundworker notwendig der das asynchron erledigt.

Implementierung

Damit die Ereignisse alle Änderungsinformationen mitliefern können also zunächst mal eine TriggerArgs Klasse. Damit diese universell eingesetzt werden kann ist die id als string angegeben.

using MongoDB.Bson;
 
namespace ReactiveMongo
{
  
class MongoTriggerArgs
  {
    
public string Context { get; private set
; }
    
public string Operation { get; private set
; }
    
public string Id { get; private set
; }
    
public BsonDocument Document { get; private set
; }
     public MongoTriggerArgs(string context, string operation, string id, BsonDocument document)
     {
       Context = context;
       Operation = operation;
       Id = id;
       Document = document;
     }
    
public override string
ToString()
     {
      
return string.Format("{0}:{1}[{2}]", Operation, Context, Id);
     }
   } }

Als Context wird die Datenbank und die Collection mit Punkt getrennt angegeben (siehe oplog). Operation wird als “u”, “i”, “d” für update, insert, delete angegeben.

In Document steht der gesamte neue Datensatz zur Verfügung.

Hier also die Implementierung:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
 
namespace ReactiveMongo
{
  
class MongoTrigger : IObservable<MongoTriggerArgs>, IDisposable
  {
    
private readonly List<IObserver<MongoTriggerArgs
>> observers;
    
private readonly MongoCollection
opLog;
    
private readonly BackgroundWorker
trigger;
    
public MongoTrigger(MongoClient
client)
     {
       observers =
new List<IObserver<MongoTriggerArgs
>>();
      
var local = client.GetServer().GetDatabase("local"
);
       opLog = local.GetCollection(
"oplog.rs"
);
       trigger =
new BackgroundWorker { WorkerSupportsCancellation = true
};
       trigger.DoWork += TriggerOnDoWork;
       trigger.RunWorkerAsync();
     }
    
private void TriggerOnDoWork(object sender, DoWorkEventArgs
doWorkEventArgs)
     {
var tmax = opLog.FindAllAs<BsonDocument>()
.SetSortOrder(SortBy.Descending(new string[] {"ts"}))
.SetLimit(1)
.First();
var timeStamp = (BsonTimestamp)tmax["ts"];
      
while
(!trigger.CancellationPending)
       {
        
var cursor = opLog.FindAs<BsonDocument>(Query.GTE("ts"
, timeStamp))
                     .SetFlags(
QueryFlags.AwaitData | QueryFlags.TailableCursor | QueryFlags
.NoCursorTimeout)
                     .SetSortOrder(
SortBy.Ascending("$natural"
));
        
using (var enumerator = (MongoCursorEnumerator<BsonDocument
>)cursor.GetEnumerator())
         {
enumerator.MoveNext();
          
while
(!trigger.CancellationPending)
           {
            
if
(enumerator.MoveNext())
             {
              
var
document = enumerator.Current;
              
if (document != null
)
               {
                
Debug
.WriteLine(document.ToString());
                
var context = document["ns"
].AsString;
                
var operation = document["op"
].AsString;
                
var doc = document["o"
].AsBsonDocument;
                
var id = doc["_id"
].AsBsonValue.ToString();
                 NextTrigger(
new MongoTriggerArgs
(context, operation, id, doc));
                 timeStamp = (
BsonTimestamp)document["ts"
];
               }
             }
            
else
            {
              
if
(enumerator.IsDead)
               {
                
break
;
               }
              
if
(!enumerator.IsServerAwaitCapable)
               {
                
Thread.Sleep(TimeSpan
.FromMilliseconds(100));
               }
             }
           }
         }
       }
     }
    
public IDisposable Subscribe(IObserver<MongoTriggerArgs
> observer)
     {
      
if
(!observers.Contains(observer))
         observers.Add(observer);
      
return new Unsubscriber
(observers, observer);
      }
    
private class Unsubscriber : IDisposable
    {
      
private readonly List<IObserver<MongoTriggerArgs
>> unsubscribeObservers;
      
private readonly IObserver<MongoTriggerArgs
> unsubscribeObserver;
      
public Unsubscriber(List<IObserver<MongoTriggerArgs>> observers, IObserver<MongoTriggerArgs
> observer)
       {
         unsubscribeObservers = observers;
         unsubscribeObserver = observer;
       }
      
public void
Dispose()
       {
        
if (unsubscribeObserver != null
&& unsubscribeObservers.Contains(unsubscribeObserver))
           unsubscribeObservers.Remove(unsubscribeObserver);
       }
     }
    
private void NextTrigger(MongoTriggerArgs
args)
     {
      
foreach (var observer in
observers)
       {
         observer.OnNext(args);
       }
     }
    
public void
EndTransmission()
     {
      
foreach (var observer in
observers.ToArray())
        
if
(observers.Contains(observer))
           observer.OnCompleted();
       observers.Clear();
     }
    
public void
Dispose()
     {
      
if (trigger != null)
       {
         trigger.CancelAsync();
       }
     }
   } }

Dies ist die erste Version der Implementierung, in ein paar Stunden erstellt. Ich denke, dass sich beim konkreten Einsatz noch Änderungen und Erweiterungen ergeben.

Link: Daniel Weber: HACK: Creating triggers for MongoDB

Ich bin dankbar für Anregungen…

Veröffentlicht unter Uncategorized | 1 Kommentar

NuGet Addin für MonoDevelop auch unter Ubuntu 12.10

Dank Matt Ward ist ein NuGet Addin für MonoDevelop verfügbar. Er hat es von der SharpDevelop Implementierung für MonoDevelop portiert. Es steht auf GitHub zum Download bereit.
Leider kann es erst ab Mono Version 3.0.5 installiert werden. Verwendet man MonoDevelop unter dem aktuellen Ubuntu 12.10 (Quantal Quetzal) ist MonoDevelop leider nur in der Version 3.0.3 verfügbar.
Ich habe das Addin für diese Version neu erstellt und ein entsprechendes Installationspaket zusammengestellt. Das ermöglicht den Einsatz unter Ubuntu und vereinfacht die Installation.

Update 10.2.2013: NuGet Addin V0.4
Download hier (Installationspaket für Ubuntu)

Veröffentlicht unter Uncategorized | 9 Kommentare

Siemens liefert Barbie-Puppen

Wir schreiben das Jahr 2021. Wieder erwarten ist die Welt vor neun Jahren nicht untergegangen. Die Mayas meinten mit dem bevorstehenden Ende nicht den Untergang der Welt wie wir sie kennen sondern nur das Ende einer Ära.

Nachdem sich Microsoft erfolgreich von der Herstellung von Business-Software verabschiedet hat und nun Consumer-Produkte vertreibt können sich andere Firmen nicht mehr halten!

Der Vorstand der Siemens AG hat festgestellt, dass in anderen Marktsegmenten wie der Spielzeugbranche Milliardenumsätze gemacht werden. In diesem Marktsegment hat Siemens bis dato keinerlei Marktanteil. Eine globale Umorientierung soll dies ändern.

Der von internen Uneinigkeiten mitgenommene Vorstand hat entschieden, dass ab der nächsten Produktserie neben den Retro-Produkten die entsprechenden Spielzeuge geliefert werden. Ergänzt wird das Angebot durch die neuen Simone-Puppen.

Die Altvorderen sagten zu so was schon: Schuster bleib bei deinen Leisten…

Veröffentlicht unter Uncategorized | Kommentar hinterlassen

Meine! Windows Roadmap

Jede Projektmanagement-Strategie fordert, dass zunächst ein Ziel/Produkt definiert werden soll, damit alle Beteiligten wissen wo die Reise hin gehen soll.

image

Was Entwicklern für Windows momentan fehlt ist eben diese Ziel!

Der große Trampel Steve hat dazu nur die Worte: Windows, Windows, Windows, … (nur lauter und mit aufstampfen)

Angefangen hat das Drama mit Windows Phone 7, das auch mit Mango keine wirkliche Bereicherung ist. Lieblos implementierte Features und teilweise fehlende Klassenimplementierungen. Ja, ich weiß, “Delivery is also a feature” – aber es sollte nicht das Einzige sein.

Jetzt stehen die WP7 Entwickler vor einem Scherbenhaufen und keiner will sagen wie es damit weiter gehen soll. Laufen denn WP7 Anwendungen auf WP8???

Ohne ein Ziel hat doch keiner Lust auf so eine Plattform zu setzen.

Die Fortsetzung kommt jetzt auf den Desktop. Was auf einem Tablett ganz gut funktioniert aber nicht auf meinem 30” am Arbeitsplatz. Acht mal acht Zentimeter große Kacheln und eine umständliche Bedienung mit der Maus.

Aus Entwicklersicht habe ich immer noch nicht klären können was nun mit .NET Anwendungen ohne WinRT auf ARM Plattform möglich sein wird. Und der unsägliche Schlingerkurs um .NET. Vor 10 Jahren begonnen hat man damals wohl alle Gegenargumente gesammelt, um sie jetzt selbst wieder anbringen zu können.

Also hier MEINE Roadmap:

  WP7 / WP8 Windows 8
Q2 2012 Marktanteil 1,4% fallend Consumer Preview
Q3 2012 Ankündigung WP8 Release Preview
Q4 2012   Release
Q1 2013 Marktanteil unter 1% Mehrere Milliarden in Werbung versenken
Q2 2013   Windows 8 Classic

Während ich WP7 keine weitere Beachtung schenke freue ich mich doch auf Windows 8 Classic, dann kann ich wieder vernünftig arbeiten.

Veröffentlicht unter Uncategorized | 1 Kommentar

INotifyPropertyChanged

Act 1 Scene 2 – Naming Conventions

Thinking a lot about INotifyPropertyChanged I visited Dan Wahlin’s and John Papa’s talks on the DevConnections in Karlsruhe/Germany. They also “discussed” about the Naming convention 😉

I, for my opinion, see the following naming as the best:

  class MyViewModelBase : INotifyPropertyChanged

  {

    public event PropertyChangedEventHandler PropertyChanged;

 

    public void NotifyPropertyChanged(string name)

    {

      var handeler = PropertyChanged;

      if(handeler != null)

        handeler(this, new PropertyChangedEventArgs(name));

    }

  }


The consumer of the event uses OnPropertyChanged as of a button click, the handler would be OnClick.

  class MyViewModel : MyViewModelBase

  {

    public MyViewModel()

    {

      PropertyChanged += OnPropertyChanged;

    }

    void OnPropertyChanged(object sender, PropertyChangedEventArgs e)

    {

      throw new NotImplementedException();

    }

  }

So why I think it should be this way? Think about an interface ISayHello.

  interface ISayHello

  {

    event Action Hello;

  }

The method to fire the event should be SayHello. Nobody would say FireSayHello, RaiseSayHello or anything in the real world. What else useful than raise/fire could be done with an event? So those words are redundant.

  class HelloSayer : ISayHello

  {

    public event Action Hello;

    public void SayHello()

    {

      var handler = Hello;

      if(handler != null)

        handler();

    }

  }
The listener of again uses OnHello to handle its listening.
  class Listener

  {

    private readonly HelloSayer friend;

    public Listener()

    {

      friend = new HelloSayer();

      friend.Hello += OnHello;

    }

    private static void OnHello()

    {

      Debug.Print("Goodby");

    }

  }

Conclusion

Every time an interface is like “INotifyPropertyChanged” => “I notify (you for every) property changed” the rising method should be the name without the interface-I and the event itself should be named like the fact the event informs about.

To be discussed (#nossued)

Veröffentlicht unter DotNetGerman Bloggers, Softwareentwicklung | Verschlagwortet mit , | 2 Kommentare

Reactive Extensions können gesprächig sein

Ok, manchmal sind die Reactive Extensions sehr gesprächig im vergleich zur “klassischen” Programmierung.

Die Aufgabe: Auf das Event NotifyPropertyChanged mit dem PropertyNamen “PicturePath” hören und eine Liste neu füllen.

Klassisch

Handler einhängen

PropertyChanged += HandlePicturePathChanged;

Prüfung in der Methode.

void HandlePicturePathChanged(object sender, PropertyChangedEventArgs e)
{
  if (e.PropertyName != „PicturePath“)
    return;

Reactive

Subscription auf Event-Handler

Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(h => PropertyChanged += h, h => PropertyChanged -= h).
Where(e => e.EventArgs.PropertyName ==
„PicturePath“).Subscribe(_ => HandlePicturePathChanged());

Die Methode kümmert sich nur um “ihre Dinge”.

void HandlePicturePathChanged()
{

Meinung (gefühlt)

Der reaktive Ansatz ist besser im Sinne von “Seperation of Concerns”, dafür aber sehr gesprächig. Da ich ungerne viel tippe bin ich in diesem Fall nicht überzeugt.

Schöner wird es, wenn man auf mehrere Teilmengen des Eventstroms wartet.

var propChanged =

Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(h => PropertyChanged += h, h => PropertyChanged -= h);

propChanged.Where(e => e.EventArgs.PropertyName == „PicturePath“).Subscribe(_ => HandlePicturePathChanged());

propChanged.Where(e => e.EventArgs.PropertyName == „ActiveProject“).Subscribe(_ => HandleActiveProjectChanged());

(Ich mag Rx) 

Veröffentlicht unter DotNetGerman Bloggers, Softwareentwicklung | Verschlagwortet mit , | Kommentar hinterlassen

MVVM DynamicViewModel

Ok, was will man denn eigentlich?
Man hat schon die Daten in einem Model wie z.B.

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

und benötigt dazu ein ViewModel.
Microsoft sieht das so: Handarbeit!

class MsViewModel : INotifyPropertyChanged
{
  private readonly Customer _customer;
  public MsViewModel(Customer customer)
  {
    _customer = customer;
  }

  public string FirstName
  {
    get { return _customer.FirstName; }
    set
    {
      if (value == _customer.FirstName)
        return;
      _customer.FirstName = value;
      NotifyPropertyChanged(„FirstName“);
    }
  }

  public string LastName
    . . .

  #region INotifyPropertyChanged Members
  public event PropertyChangedEventHandler PropertyChanged;
  protected void NotifyPropertyChanged(string name)
  {
    var handler = PropertyChanged;
    if (handler == null)
      return;
    handler(this, new PropertyChangedEventArgs(name));
  }
  #endregion
}

Das ist Fleißarbeit, Fehleranfällig, nicht refakturierbar, ätzend, usw..

Alternativ könnte ein Codegenerator geschrieben werden – mir persönlich nicht ausreichend. Ich würde das gerne so sehen:

class MyViewModel : DynamicViewModel<Customer>
{
}

Weitere Ziele der Entwicklung:

  • Blendability (Bindungen werden in Expression Blend angezeigt)
  • Automatisches INotifyPropertyChanged
  • Mehrere Modelle können in einem ViewModel zusammengefasst werden
  • Zusätzliche Properties als Ergänzung zu denen des/der Models
  • Aktualisierung voneinander abhängiger Properties
  • Filterung der Properties (nicht alle werden dem View bereitgestellt

Los gehts. Erst mal die Grundfunktion, also die Properties des Models im View bereitstellen.

Der erste Ansatz ist ExpandoObject – geht aber nicht weil sealed, eine Schutzbehauptung im Sinne von: Oh, das ist schon gut, aber ob es so bleiben kann weiß ich noch nicht Zwinkerndes Smiley

Also direkt von DynamicObject ableiten. Mehr Arbeit aber geht – fast. Der Designer kann jetzt die Bindungen nicht mehr Anzeigen.

Blendability

Eine Hauptforderung ist, dass das Ergebnis mit Expression Blend bearbeitet werden kann. Ohne dies ist die Zusammenarbeit mit einem Designstudio nicht möglich. Leider ergab diese Forderung die ersten Probleme.

Die Lösung ist die zusätzliche Implementierung von ICustomTypeDescriptor.

Automatisches INotifyPropertyChanged

Ergibt sich ganz einfach in der Implementierung von TrySetMember aus DynamicObject.

Mehrere Modelle

Im einfachsten Fall basiert ein ViewModel nur auf einem Model. Aber schon eine einfache Beziehung erfordert die Bindung an mehrere Modelle (z.B. Person und Firma).

Die Implementierung als Generic ist für diesen Fall nicht flexibel genug.

DynamicViewModel<Customer>

Ich habe mich deshalb entschieden eine Methode AddModel einzuführen. Daraus ergibt sich außerdem der Vorteil, dass bereits vorhandenen Instanzen des Models im ViewModel verwendet werden können.

Zusätzliche Properties

Als Ergänzung zu den Properties des Models können im angeleiteten ViewModel neue Properties eingeführt werden. Ich habe hierbei zwei Varianten implementiert.

class MyViewModel : DynamicViewModel
{
  public string MyProp { get; set; }

Die public Properties der Ableitung stehen ganz normal zur Verfügung. Mit automatischer Unterstützung von INotifyPropertyChanged!!!

Außerdem können “noch dynamischer” über einen Indexer zur Laufzeit hinzugefügt werden.

public object this[string name]

this[„Test“] = „kuckuck“;

Werden diese im Konstruktor hinzugefügt stehen sie auch im Designer zur Verfügung!!!

Binding

Aktualisierung voneinander abhängiger Properties

Im speziellen ViewModel will man gewöhnlich weitere, aus den Daten des zugrunde liegenden Models berechnete Properties bereit stellen. Hierbei gibt es folgende Probleme. Erstens ist zur Kompilierzeit der Typ der/des Models nicht bekannt. Der Zugriff muss also über den Indexer erfolgen (nicht schön, ich bin noch am Grübeln). Außerdem muss zur korrekten Weiterleitung von  INotifyPropertyChanged bekannt sein, auf welchen Properties das neue basiert. Hierzu wird das Attribut DependsOn verwendet. Gibt es schon im Framework, muss aber hier für die eigenen Zwecke auch selbst implementiert werden.

[DependsOn(„FirstName“)]
public int LenOfFirstName { get { return this[„FirstName“].ToString().Length; } }

Filterung der Properties

Um nicht alle Properties des Models im Designer sichtbar zu machen können die zu versteckenden mit dem Browsable(false) Attribut gekennzeichnet werden.

[Browsable(false)]

Gibt es auch schon im Framework, muss aber auch hier für die eigenen Zwecke auch selbst implementiert werden.

Fazit

Für den ersten Moment bin ich mit der Funktionalität sehr zufrieden.

Auf dem OpenSpace im Juli in Karlsruhe werde ich auf jeden Fall eine Runde darüber diskutieren…

 


 

Veröffentlicht unter DotNetGerman Bloggers, Softwareentwicklung | Verschlagwortet mit , | 1 Kommentar