Xamarin.Forms im Praxiseinsatz: Wie viel Code Sharing ist realistisch? 22.05.2015

Markus Demmler
Markus Demmler, Principal eXpert

Einer der Vorteile von Xamarin.Forms gegenüber Xamarin.iOS und Xamarin.Android ist die plattformübergreifende Verwendung von Views. Dadurch wird der jeweilige Mehraufwand pro zu unterstützende Plattform geringer. Dieser Artikel beschreibt, welchen Prozentsatz an Code Sharing wir in einem aktuellen Xamarin.Forms Kundenprojekt realisieren konnten und gibt zusätzlich Tipps zum Erreichen eines solchen Prozentsatzes.

 

Code-Strukturierung in einem Xamarin.Forms Projekt

Im Allgemeinen besteht eine Xamarin.Forms Solution aus jeweils einem Projekt pro zu unterstützende Plattform und einem (oder mehreren) plattformübergreifenden Projekten. Für die plattformübergreifenden Projekte gibt es zwei unterschiedliche Lösungsansätze:

  • Shared Projects
  • Portable Class Libraries (PCL)

Shared Projects liegt die Idee zu Grunde, den enthaltenen Code für die jeweilige Plattform zu kopieren und erneut zu übersetzen. Auf diese Weise wird keine neue Assembly erzeugt, sondern der Code direkt in die Assembly des referenzierenden Projekts integriert. So kann der Code dieser Assembly auf mehreren Plattformen wiederverwendet werden und zugleich über Präprozessordirektiven oder partielle Methoden und Klassen in den nativen Projekten auf plattformspezifische Anforderungen eingegangen werden, ohne zusätzliche Abstraktionsschichten für die nativen Funktionen einbauen zu müssen. Solch ein Projekt hat allerdings den Nachteil, dass aufgrund der fehlenden oder zu starken Codetrennung die Übersicht im Code und somit die Wartbarkeit schnell verloren geht.

PCLs hingegen bieten die Möglichkeit, mit gemeinsamer Funktionalität ausgewählter Zielplattformen gemeinsamen Code in einer Assembly abzubilden. Da in dem Kundenprojekt das innerhalb einer Xamarin-kompatiblen PCL verfügbare Subset des .NET Frameworks ausreichend war und die meisten benötigten nativen Features als Plugins vorhanden waren, wurde der PCL Ansatz gewählt. Um ein möglich effizientes Code Sharing mit einer nativen Windows 8 App zu erreichen, wurde der plattformübergreifende Code auf zwei Projekte aufgeteilt. Diese Trennung hat es ermöglicht, Business-Logik und ViewModels mit einem alternativen UI Framework (in diesem Fall WinRT) wiederzuverwenden.

architektur

Die einzelnen Projekte beinhalten:

  • Portable-Projekt (PCL):
    • Business-Logik
    • ViewModels
  • Forms-Projekt (PCL):
    • Views
    • Converter
    • Plattformunabhängige Custom Controls
  • Forms.iOS (nativ) und Forms.Android (nativ) Projekt:
    • Plattformspezifische Startdatei (iOS: AppDelegate.cs, Android: MainActivity.cs)
    • Plattformspezifische Custom Control Renderer
    • Plattformspezifische Services

 

Code Sharing

Der folgende Screenshot zeigt die Aufteilung der Lines of Code auf die einzelnen Projekte:

lines of code

Für die iOS App ergibt sich damit ein plattformabhängiger Anteil von 13% (Forms.iOS / (Portable + Forms + Forms.iOS)). Für die Android App ergibt sich ein plattformabhängiger Anteil von 6% (Forms.Android / (Portable + Forms + Forms.Android)). Hierbei ist aber zu beachten, dass es sich bei den 13% bzw. 6% nicht um komplett selbst geschriebenen Code handelt, sondern vor allem um die Implementierung der benötigten Device Features (in dem Kundenprojekt zum Beispiel der Dateizugriff). Hierzu gibt es mehrere passende Quellen für alle gängigen Device Features:

Der tatsächlich plattformspezifische, selbst geschriebene Code war vor allem die Implementierung von  Custom Control Renderer, um die von Xamarin.Forms vorgegebene Visualisierung von UI Controls zu modifizieren. Ein Beispiel hierfür ist ein zusätzlich unter iOS notwendiger Disclosure Indicator für Listenelemente.

Fazit

Durch den Einsatz von Xamarin.Forms und einer durchdachten Code-Struktur ist es möglich, den plattformabhängigen Code auf ein Minimum zu reduzieren und dennoch native Oberflächen zu implementieren.

Share |

Testen von Datenbanken - 7 Fazit 20.05.2015

Alexander Kabisch
Alexander Kabisch, Principal eXpert

Der Code der Datenbank gehört zur Anwendung, damit sind auch Datenbanktestfälle für eine saubere Entwicklung essentiell.

In den vielen Projekten schrecken nicht Nachteile der einen oder anderen Lösung ab, sondern alleine der Aufwand. Tests aufzusetzen und zu pflegen kostet Zeit. Leider vergessen die meisten den Aufwand der durch Fehler und deren Suche entsteht.
Sollte es doch Testfälle geben, so werden in sehr vielen Projekten die Tests gegen die Entwicklungs- oder eine separate Testdatenbank ausgeführt. Aber es gibt weitaus elegantere Möglichkeiten seine Anwendung zu testen, als diese beiden.

Ich habe hier drei Grundvorgehen

  • Testdaten liegen vor und Änderungen durch Tests werden in der Datenbank persistiert
    • Testfälle in der Entwicklungsdatenbank
    • Testfälle in separater Testdatenbank
  • Testdaten liegen als Snapshot vor, der immer wieder neu und sauber verwendet wird
    • Testfälle in zur Laufzeit gemounteter Datenbank
  • Testdaten werden in eine immer wieder neu erstellte Datenbank eingespielt
    • Testfälle in zur Laufzeit generierter Datenbank

mit einer Erweiterung

kombiniert. Ich bin zwar nur auf die Kombination des TransactionContextes mit der Entwicklungsdatenbank eingegangen, aber genauso sind andere Zusammensetzungen möglich.

  • Bereits vorgestellte Möglichkeit
    1. Entwicklungsdatenbank verwenden
    2. TransactionContext  erstellen
    3. Löschen aller unnötigen Daten
    4. Testdaten einspielen
    5. Test durchführen
    6. Ergebnisse prüfen
    7. Rollback durchführen
  • Weitere Möglichkeit
    1. Mit Testdaten gefüllte Datenbankdatei oder Testdatenbank verwenden (Snapshot)
    2. TransactionContext  erstellen
    3. Test durchführen
    4. Ergebnisse prüfen
    5. Rollback durchführen
  • Weitere Möglichkeit
    1. Einmalig im Test generierte Datenbank oder leere Datenbank verwenden
    2. TransactionContext  erstellen
    3. Testdaten einspielen
    4. Test durchführen
    5. Ergebnisse prüfen
    6. Rollback durchführen

Ich persönlich setzte gern die Variante mit der Entwicklungsdatenbank und TransactionContext ein. Hier finde ich immer den aktuellsten Softwarestand vor, auch wenn das heißt dass hin und wieder Testfälle während der Entwicklung fehlschlagen. Hauptsache sie laufen erfolgreich vor dem Release durch!

Kleine Tipps zur Fehlersuche

Durch den Rollback am Ende des Tests kann man nicht mal eben die Ergebnisdaten in der Datenbank prüfen. Sie werden ja nicht persistiert, deshalb:

  • Testfälle sollten “überschaubar” sein, also bitte nicht die halbe Datenbank in zwei Testfällen testen.
  • Testfälle sollten niemals auf Ergebnissen andere Testfälle aufbauen, sondern diese benötigten Testdaten beim Start selbst einspielen
  • Testfälle sollten so viele Ergebnisse wie möglich rausschreiben, eine Prüfung im Code mit dem Ergebnis “fehlerhaft” ist meist nicht sehr hilfreich
    • Im Beispiel habe ich immer das Resultset der Prüfabfrage herausgeschrieben. Vielleicht hilft manchmal auch das Rausschreiben einer Eingangstabelle weiter.

Zur Analyse des Fehlers können mit etwas Aufwand fast alle Testfälle auch in entsprechende SQL Skripte umgewandelt werden:

BEGIN TRANSACTION
 
TRUNCATE TABLE [dbo].[Source] 
TRUNCATE TABLE [dbo].[Target_Sum] 
TRUNCATE TABLE [dbo].[Target_Code] 
 
 
INSERT INTO [dbo].[Source] ( [CalcDate],[Code],[Value]) VALUES ('2014-01-01','A','10');
...
 
EXEC [dbo].[Insert_Target_ByCalcDate] '2014-01-01'
 
SELECT * FROM [dbo].[Source] 
SELECT * FROM [dbo].[Target_Sum] 
SELECT * FROM [dbo].[Target_Code] 
 
ROLLBACK TRANSACTION

Aber im Endeffekt lässt sich sagen, selbst langsame und von Entwicklungsdaten beeinflusste Tests sind besser als keine. Es reicht sie am Ende der Entwicklung vor dem Release zum Laufen zu bringen.

zur Übersicht

Share |

Diagnose: Akute Frameworkeritis 13.05.2015

Torben Graefe
Torben Graefe, Senior eXpert

Es ist noch nicht so lange her, da war JavaScript für viele .NET-Entwickler ein eher unappetitliches Nebenprodukt von ASP.NET WebForms. Die Ursünde von JavaScript steht in einer Reihe mit der Darreichung des verbotenen Apfels vom Baum der Erkenntnis: JavaScript ist nicht streng typisiert und bietet Freiheiten, nach denen der .NET-Entwickler nie gefragt hat. Obendrein beschleicht ihn schon nach seiner ersten Zeile JavaScript-Code das ungute Gefühl, sich einen nicht zu testenden Wartungsalptraum eingehandelt zu haben, der ihn bis in den Schlaf verfolgen wird.

Nicht viel anders verhält es sich mit HTML und CSS, die dem gestandenen .NET-Entwickler wie die hässliche Nachgeburt des Internetzeitalters erscheinen. Damit konstruierte Seiten besitzen nach seiner Erkenntnis die Unart, sich prinzipiell völlig anders als gedacht darzustellen – und das sowieso noch einmal unterschiedlich in jedem Browser, wenn nicht gar in jeder Browserunterversion.

Umso deprimierender mag es für den .NET-Entwickler sein, dass dieses Konglomerat aus JavaScript und HTML/CSS, welches aus seiner Sicht längst zurecht von der Bildfläche hätte verschwinden sollen, sich auf eben dieser Bildfläche breitmacht wie kein anderer Technologie-Stack. Was soll der .NET-Entwickler tun, wenn er von seinem Arbeitgeber oder Kunden zu dem aberwitzigen Unterfangen getrieben wird, ausgerechnet in den Giftschrank der clientseitigen Web-Alchemie zu greifen, um eine Software von nennenswerter Stabilität zu entwickeln?

"Das Blau ist mir nicht blau genug", schreibt mein Kollege Sven Martens. Ach, wenn es denn nur das Blau wäre! Manch ein Backend-affiner .NET-Entwickler, der bislang erfolgreich einen Bogen um das ganze "JavaScript-Geraffel" gemacht hatte, sieht bei seinen ersten, von leidenschaftlicher Verachtung begleiteten Schritten in der bunten Welt der vermeintlich modernen Web-Technologien vor allem Rot.

Rettung ist in Sicht

Doch unser .NET-Entwickler ist längst nicht der einzige, der den Webtechnologie-Wildwuchs am liebsten mit Stumpf und Stiel ausreißen würde. Kluge Köpfe in aller Welt haben Ideen und Konzepte entwickelt, um diesen auf ein erträgliches Maß zurecht zu stutzen, bis JavaScript zumindest rudimentär einer höheren Programmiersprache ähnelt. Typisierte Variablen müssen her, Vererbungshierarchien für dies und das, Model Bindings in alle erdenklichen Richtungen, eine Art querybasierende Abfragesprache, Code-Beautifier, Code-Minifier, Paketierer und so weiter.

Mehrmals in der Woche läuft der Arbeitsrechner heiß, wenn die neuesten Schmerzlinderungen über NuGet ihren Weg ins Visual-Studio-Projekt finden – oder zumindest ein Teil davon, beim Rest muss man dann noch selbst etwas Hand anlegen. Aber das ist ja kein Problem, man trägt sich einfach kurz in eine Mailing-Liste ein und schon wird man informiert, wann der nächste Nightly Build zum Download bereitsteht. Ab und zu – aber wirklich nur ganz selten – mag es vorkommen, dass ein Contributer aus Pakistan, Kirgisien oder der sächsischen Schweiz es bei seinem hehren Ansinnen, den Code nach dem Pfadfinderprinzip schöner zurückzulassen als er ihn vorgefunden hat, mit der Rückwärtskompatibilität nicht so genau nimmt. Oder es wird ein neues Feature eingebaut, das leider nur zu 99% mit einem anderen arbeitserleichternden Framework kompatibel ist. Alles halb so schlimm – denn auf der im Quellcode angegeben Webseite des jamaikanischen Autors, der scheinbar auch eine Surfschule und ein Blog für lustige Katzenbilder betreibt, heißt es schon seit einer Woche sinngemäß, dass eine Lösung in Arbeit wäre.

Wo gehobelt bzw. geframeworkt wird, da fallen Späne. Daran werden sich die auch Kollegen von der internen IT, denen nach Projektende die dankbare Aufgabe zufällt, dieses schöne Stück Software am Laufen zu halten und punktuell zu erweitern, schnell gewöhnt haben.

Auf zu neuen Ufern

Auf unseren .NET-Entwickler wartet indes die nächste Herausforderung, womöglich in Form eines weiteren Web-Projektes. Aber damit kennt er sich nun aus – dachte er zumindest, bis er feststellte, dass in seinem neuen Projekt nur 3 der 21 Frameworks und Tools zum Einsatz kommen, mit denen er sich sein erstes Web-Projekt erträglich gemacht hat. Dafür hat ein anderer Entwickler bereits vier Monate nach Projektstart 16 andere Helferlein eingebunden, die allesamt ähnlich vielsprechend und vergleichbar nützlich sind. Man versteht sich unter Gleichgesinnten, auch wenn man über die eine oder andere Entscheidung noch einmal diskutieren müsste. Da gäbe es zum Beispiel dieses Productivity Tool, mit dem der .NET-Entwickler im letzten Projekt schon ein paar Nächte lang Erfahrung gesammelt hat. Das hat nämlich dieses eine geniale Feature, ohne dass man heutzutage gar nicht mehr auskommen kann. Zum Glück ist der andere Entwickler in diesem Punkt ganz offen und es macht ihm auch nichts aus, dass die bereits vorhandene Code-Basis dafür etwas umgestrickt werden muss. Im Grunde wollte er sich sowieso noch in dieses neue Tool einarbeiten, es fehlte ihm bislang einfach nur die Zeit dafür. Die Bedienung der anderen Tools und Frameworks erlernt sich ja auch nicht von selbst.

Außerdem hat er Ärger mit dem Kunden, weil seit dem letzten Skript-Update in unregelmäßigen Abständen ein Testbild mit einer surfenden Katze in der Produktgalerie erscheint. Aber auch dieses Problemchen wird nach der Einführung des neuen Tools bestimmt behoben sein.

Share |

LINQ Coding Guidelines #12–Anonyme Klassen 11.05.2015

Alexander Jung
Alexander Jung, Chief eXpert

Wenn es fertige Datentypen für die Abfrage gibt – zum Beispiel durch das Entity Framework generierte Klassen – liegt deren Verwendung auf der Hand. Bei Zwischenergebnissen oder trivialen Ausschnitten fehlen solche Datenstrukturen aber; hier kommen oft anonyme Klassen zum Einsatz.

Empfehlung:
* Der Einsatz von anonymen Datentypen ist auf einfachste(!) Fälle und Zwischenergebnisse zu beschränken (alternativ: ganz zu vermeiden).

 

Die Samples auf MSDN machen es vor, sogar mit expliziten Beispielen:

   1: var upperLowerWords = 
   2:     from w in words 
   3:     select new { Upper = w.ToUpper(), Lower = w.ToLower() }; 
   4:  
   5: var digitOddEvens = 
   6:     from n in numbers 
   7:     select new { Digit = strings[n], Even = (n % 2 == 0) }; 
   8:  
   9: var productInfos = 
  10:     from p in products 
  11:     select new { p.ProductName, p.Category, Price = p.UnitPrice }; 

Das ist in Ordnung für Zwischenergebnisse (was voraussetzt, das eine weitere Verarbeitung unmittelbar danach folgt – was in den Beispielen aber nicht gegeben ist!).

Auch wenn man die Verarbeitung oft in den gleichen Ausdruck mit aufnehmen könnte, gibt es valide Gründe für die Trennung. Das Verhindern von redundanten Datenbankabfragen oder wiederholten Neuberechnungen gehört ebenso dazu, wie das Trennen anhand der Provider oder ganz einfach die Lesbarkeit.

Anonyme Typen machen dieses Vorgehen sehr einfach. Von einfachen Fällen abgesehen gibt es jedoch gute Gründe, die Datentypen explizit zu definieren.

Schauen wir uns als Beispiel den RayTracer an:

   1: var pixelsQuery =
   2:     from y in Enumerable.Range(0, screenHeight)
   3:     ...
   4:     select new { X = x, Y = y, Color = traceRay(new TraceRayArgs(ray, scene, 0)) };
   5:  
   6: foreach (var row in pixelsQuery)
   7:     foreach (var pixel in row)
   8:         setPixel(pixel.X, pixel.Y, pixel.Color.ToDrawingColor());

Für jedes Objekt, das als Parameter benötigt wird oder als Zwischenergebnis entsteht – Ray, TraceRayArgs, Scene, … – gibt es eine eigene Klasse. Und ausgerechnet das Ergebnis der Berechnung hat diese Aufmerksamkeit nicht verdient? (Rhetorische Frage!)

Oder hier:

   1: public static IEnumerable<object> GetSystemsForApplication(this SystemRepository repository, string applicationId, bool includeWithdrawn)
   2: {
   3:     return repository.ServersThisApplicationDependsOn
   4:         .Where(item => item.CONSUMING_APPLICATION == applicationId)
   5:         .Where(item => includeWithdrawn || (item.DEPENDENCY_STATUS != WithDrawnIdentifier))
   6:         .OrderBy(item => item.SERVER_NAME)
   7:         .AsEnumerable()
   8:         .Select(item => new
   9:         {
  10:             item.CONSUMING_APPLICATION,
  11:             item.SERVER_NAME,
  12:             item.DEPENDENCY_STATUS,
  13:             item.SERVER_STATUS,
  14:             item.SERVER_OS,
  15:             item.SERVER_LOCATION,
  16:         });
  17: }

Aufgrund der Arbeitsweise – Rückgabe als object – ist ein Datentyp zumindest technisch nicht notwendig. Wäre er nicht trotzdem sinnvoll, um konsistent an verschiedenen Stellen verwendet werden zu können? Oder – immerhin ist er fachlich relevant – um die Datenstruktur explizit zu dokumentieren und damit auch bei der Eingabe durch Intellisense zu unterstützen? (Wieder: Rhetorische Fragen!)

Ergo: Sieht man von trivialen Zwischenergebnissen innerhalb einer Abfrage ab, dann ist die Verwendung von explizit definierten Datentypen einem anonymen Datentyp fast immer vorzuziehen.

Share |

Testen von Datenbanken - 6 Testfälle mit TransactionContext 08.05.2015

Alexander Kabisch
Alexander Kabisch, Principal eXpert

Bei diesem Vorgehen werden die Testfälle gegen eine Datenbank mit aktueller Struktur ausgeführt. Für den Test wird ein TransactionContext erstellt, darin werden Entwicklungsdaten gelöscht, Testdaten per Script eingespielt und der Test ausgeführt. Am Ende wird dann die Transaktion zurückgerollt, wodurch die Datenbank unverändert bleibt.

Mit diesem Vorgehen ist es zwar möglich jede Datenbank zu schützen, aber Vorzugweise verwende ich die Entwicklungsdatenbank.

Testablauf am Beispiel der Referenzanwendung

  1. Ein TransactionContext wird erstellt.
  2. Die Entwicklungsdaten werden gelöscht.
  3. Die Testdaten zum 1.1.2014 werden in die Tabelle [dbo].[Source] eingespielt.
  4. Die Tabelle [dbo].[Target_Sum] wird gelöscht.
    • [dbo].[Delete_Target_Sum_ByCalcDate]
  5. Die Tabelle [dbo].[Target_Sum] wird befüllt.
    • [dbo].[Insert_Target_ByCalcDate]
  6. Es wird geprüft ob die Daten wie erwartet in der Tabelle [dbo].[Target_Sum] vorliegen.
    • Die Daten werden vom Test in eine Datei exportiert.
    • Die Datei wird mit einer erwarteten Datei verglichen.
  7. Der TransactionContext wird zurückgerollt.
TransactionScope m_transactionScope;
DBManager m_testDatenmangerRoot;
 
[TestInitialize]
public void Init()
{
    m_transactionScope = new TransactionScope(TransactionScopeOption.Required);
    m_testDatenmangerRoot = new DBManager("ROOT", true);
    m_testDatenmangerRoot.ExecFile(@".\Scripte\InsertData.sql");
}
 
[TestMethod]
public void DALTest_Transaction()
{
    DBManagerTarget mgr = new DBManagerTarget();
    DateTime calcDate = new DateTime(2014, 01, 01);
 
    mgr.DeleteTargetSum(calcDate);
    mgr.FillTarget(calcDate);
 
    DBManager testDatenmanger = new DBManager("DB", true);
 
    string filename = "DALTest_Transaction_UnitTest1.txt";
    testDatenmanger.ReadToFile(
        DIRTARGET + filename,
        "SELECT * FROM dbo.Target_Sum WHERE CalcDate='2014-01-01';");
 
    CompareHelper.AssertAreEual(DIRSOURCE + filename, DIRTARGET + filename);
}
 
[TestCleanup]
public void CleanUp()
{
    //no transactionScope.Complete();
    m_transactionScope.Dispose();
    m_transactionScope = null;
    m_testDatenmangerRoot = null;
}
<connectionStrings>
  <clear/>
  <add name="ROOT" connectionString="...;Initial Catalog=DBTesting;...User Id=sa;..."/>
  <add name="DB" connectionString="...;Initial Catalog=DBTesting;..."/>
</connectionStrings>
TRUNCATE TABLE [dbo].[Source] 
TRUNCATE TABLE [dbo].[Target_Sum] 
TRUNCATE TABLE [dbo].[Target_Code] 
 
 
INSERT INTO [dbo].[Source] ( [CalcDate],[Code],[Value]) VALUES ('2014-01-01','A','10');
...

Damit die technischen IDs der Tabelle [dbo].[Target_Code] korrekt erzeugt werden ist ein TRUNCATE nötig. Hierdurch wird der Zähler des Inkrements der ID Spalte zurückgesetzt.

Ergebnis: Erfolgreich

SELECT * FROM dbo.Target_Sum WHERE CalcDate='2014-01-01';
 
CalcDate(datetime),ID(int),Value(float)
 
01.01.2014 00:00:00,1,10
01.01.2014 00:00:00,2,20
01.01.2014 00:00:00,3,30
01.01.2014 00:00:00,4,40

Da die Datenbank für jedes Testszenario neu aufbereitet wird, kann die Tabelle [dbo].[Target_Code] nie mit falschen Werten gefüllt sein.

Vorteile

  • Aktuellster Softwarestand (Bei Verwendung der Entwicklungsdatenbank)
  • Daten sind exakt auf die Test zugeschnitten
    • Entwicklungsdaten werden nicht geändert
    • Sogar IDENTITY Spalten können exakt getestet werden
  • Pro Testszenarien eigene Initialisierung möglich

Nachteile

  • Laufzeiten der Tests
  • Komplexität des Tests steigt
    • Löschen der Entwicklungsdaten
    • Referenzierte Daten werden beim Skripten leicht übersehen
  • Ggf. hoher Aufwand bei Fehlersuche, da Daten am Ende verloren gehen
  • Fatale Folgen wenn Testfälle ohne TransactionContext laufen sollten

Dieses Vorgehen ist adaptiv zu allen vorherigen zusehen. Es könnte sogar auf eine neu generierte Datenbank angewendet werden. Die meisten Vorzüge ergeben sich aber bei der Verwendung mit der Entwicklungsdatenbank. Aber auch die Kombination mit einer separaten Testdatenbank bringt viele Vorteile.

zur Übersicht

Share |

Store Deployment von Xamarin Apps 06.05.2015

Christoph Meißner
Christoph Meißner,
.NET eXpert

Von der SDX wurden bereits mehrere mobile Business-Anwendungen für die Plattformen Windows, iOS und Android sowohl für Tablets als auch für Phones entwickelt. Bisher fanden ausschließlich Anwendungen auf Basis der Windows Plattform den Weg in den Store. (z.B. SDX Worktime Pro) Dies wird sich nun ändern, da wir während unseres letzten Kundenprojektes eine Anwendung mit Xamarin für iOS u. Android entwickelt haben, die auch im Store zu finden ist. In diesem Artikel möchte ich über die von uns gesammelten Erfahrungen beim Store Deployment mit Xamarin berichten und auf die dabei aufgetauchten Probleme und Fallstricke hinweisen sowie unsere Lösungen dazu aufzeigen.

Die Entwicklung der Anwendung hat in einem Team stattgefunden, dessen Erfahrungsschatz hauptsächlich auf der .NET-Entwicklung beruht. Dank Xamarin musste dabei Visual Studio als Entwicklungsumgebung nicht verlassen werden, obwohl für die Plattformen iOS und Android entwickelt wurde. Allenfalls das einmalige Setup der Debug-Umgebung für die iOS-Anwendung, erforderte eine kurzzeitige Arbeit des Entwicklers mit einem Mac. Nachdem die Entwicklungsphase abgeschlossen war, musste die Anwendung getestet werden und sollte anschließend in die Stores deployed werden. Xamarin selbst bietet hierzu bereits je einen Guide für die beiden Stores an, wobei jeweils das Store Deployment Schritt für Schritt für iOS und auch für Android beschrieben wird. Doch war das schon alles? Musste lediglich eine „Checkliste“ abgearbeitet werden, und man hat die Anwendungen im Store? Leider fingen die Probleme mit dem Abarbeiten dieses Guides erst an.

Fallstricke beim Deployment der iOS-Anwendung

Während der Entwicklung der Anwendung wurde deren Build zum Debuggen allein von Visual Studio aus gesteuert. Jedoch besteht von Visual Studio aus nicht die Möglichkeit, ein Paket für den Store zu erstellen. Folglich muss dieses im Xamarin Studio auf dem Mac erstellt werden. Auffällig war dabei insbesondere, dass die Auswahl bei den Build-Einstellungen zwischen Visual Studio und Xamarin Studio leicht abweichend ist. In Xamarin Studio stehen beispielsweise exakt die installierten SDKs und verfügbaren Compiler-Optionen (z. B. auch ARM64 und ARM7s) zur Auswahl, während in Visual Studio nur eine pauschale Auswahl (z. B. bei den Compiler-Optionen nur ARMv6 und ARMv7) zum Debuggen, oder gar keine Auswahl – abgesehen von dem Wert „Standard“ – zur Verfügung steht. Auch weicht die Anordnung und Verteilung der Optionen voneinander ab, sodass diese in Xamarin Studio erneut zu suchen und zu prüfen sind. Dies lässt sich auch im nachfolgenden Screenshot nachvollziehen:

settings

Nach erfolgreicher Erstellung des Pakets in der Release-Konfiguration ist zusätzlich die Verwendung von Xcode erforderlich, um das Publishing in den Apple App Store durchzuführen.

Eine weitere Besonderheit der iOS App stellt die Information Property List (Datei “Info.plist”) dar. Die Info.plist ist eine Zusammenfassung von Eigenschaften der Anwendung, die in deren Paket beim Übersetzen integriert und auch zum Übersetzen verwendet sowie vom Store ausgelesen und ausgegeben werden. Die meisten dieser Eigenschaften lassen sich in der UI für die Projekteigenschaften über den Reiter iOS Application bearbeiten. Manche dieser Eigenschaften müssen jedoch von Hand in die Info.plist eingefügt werden; so etwa die Sprache, die im App Store angezeigt werden soll.

   1: <key>CFBundleLocalizations</key>
   2: <array>
   3:     <string>de</string>
   4: </array>
   5:  
   6: <key>CFBundleDevelopmentRegions</key>
   7: <string>German</string>

Fallstricke beim Deployment der Android-Anwendung

Das Deployment der Android-Anwendung in den Google Play Store hingegen hat sich zunächst einfach gestaltet. Später hat sich noch herausgestellt, dass der vom Xamarin Visual Studio Plugin verwendete Algorithmus zur Paketsignierung nicht von jedem Android-Gerät unterstützt wird. Einige Geräte gaben bei der Installation lediglich die Fehlermeldung „Package file was not signed correctly.“ zurück. Um dies zu beheben, musste das Paket unter Nutzung eines anderen Algorithmus signiert werden: In einem herkömmlichen Android-Projekt wird dazu die build.xml angepasst. In Xamarin ist dies jedoch nicht möglich. Hier muss das Paket als Release erstellt sowie von Hand mit den entsprechenden Tools aus dem jeweiligen JDK und Android SDK signiert werden. Dazu finden sich nachfolgend die erforderlichen Kommandos an unserem Beispiel mit aktuellen Tool-Versionen:

"Y:\Path\To\our\Java\jdk1.8.0_31\bin\jarsigner.exe" -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore %KeyStoreFileName%.keystore %AppPackageFileName%.apk %KeyAlias%
"Y:\Path\To\our\android-sdk\build-tools\21.1.2\zipalign.exe" -v 4 %AppPackageFileName%.apk %AppPackageFileName%.signed.apk

Abgesehen davon ist beim Android Deployment die Möglichkeit zu erwähnen, im Google Play Store spezifische Geräte zu deaktivieren, die von der Anwendung nicht unterstützt werden. Dies ist insbesondere deshalb relevant, weil es derzeit allein über 2102 Android Tablets gibt, die unterstützt werden müssen. Soll jedoch ein Formfaktor wie insbesondere z. B. von kleinen Geräten (Phablets) nicht unterstützt werden, können diese Geräte hier explizit deaktiviert werden. Möchte man diese dennoch unterstützen, bietet sich die Xamarin Test Cloud an, diese zuvor zu testen.

Fazit

Insgesamt hat sich das Store Deployment mit Xamarin problemlos hinsichtlich des Aufwands gestaltet. Selbst die Dauer des Freigabeprozesses hat sich mit ein paar Stunden bei Android und ca. zwei Wochen bei Apple relativ kurz gestaltet. Folglich ist die Anwendung nun sowohl im Apple App Store als auch im Google Play Store zu finden.

Share |

LINQ Coding Guidelines #11 – Der Einsatz von “var” 04.05.2015

Alexander Jung
Alexander Jung, Chief eXpert

Es gibt Leute, die halten var für die Quelle des Bösen, andere haben eine ausgewogenere Haltung, der eher der Intention nahe kommt.

Empfehlung:
Bei LINQ-Ausdrücken ist der Einsatz von var zu empfehlen.

 

Man mag geteilter Ansicht darüber sein, ob es Sinn macht, einfache Datentypen wie int oder string durch var zu ersetzen. Aber sobald es um Templates geht rechtfertigt sich der Einsatz von var alleine durch den Schreibaufwand. Und spätestens im Zusammenhang mit LINQ wird type inference, also die implizite Typisierung, zum beherrschenden Aspekt.

Bei LINQ ergibt sich der Typ aus der Kette der Aufrufe – und er ändert sich potentiell mit jedem Select(). Dabei ist nicht nur der Datentyp der Elemente relevant – hier bekommt var durch anonyme Typen seine besondere Rechtfertigung – sondern auch beim Typ der Enumeration, also IEnumerable<> oder IQueryable<>.

Ich zumindest baue bei der Entwicklung gerade komplexe LINQ-Anweisungen oft in einzelnen Schritten auf, wodurch sich der Typ ständig ändert. Aber auch bei fertigem Code ist eine Aufteilung in mehrere Schritte keine Seltenheit, z.B. um deutlich zu machen, dass der LINQ-Provider gewechselt wird. Wenn hier Änderungen anstehen kann sich leicht eine falsche Deklaration einschleichen, ohne dass das offensichtlich wird.

Ein (konstruiertes) Beispiel:

   1: public IEnumerable<AllocationTimeSpan> GetAllTimeSpans()
   2: {
   3:     DateTime currentDate = DateTime.Today;
   4:     IEnumerable<AllocationTimeSpan> allTimeSpans = _context
   5:         .AllocationSet
   6:         .OfType<Allocation>()
   7:         .Where(a => a.AllocatedFrom <= currentDate && a.AllocatedTo >= currentDate)
   8:         .Select(a => new AllocationTimeSpan
   9:         {
  10:             From = a.AllocatedFrom,
  11:             To = a.AllocatedTo,
  12:             Status = (Status)(a.Status),
  13:         })
  14:         .Distinct()
  15:         .OrderBy(ts => ts.From)
  16:         .ToArray();
  17:     return allTimeSpans;
  18: }

Ein Aufruf gegen die Datenbank, samt Filterung, Sortierung, etc.. Keine große Auffälligkeit. Nur findet mir etwas zu viel auf einmal statt, also trenne ich das:

   1: public IEnumerable<AllocationTimeSpan> GetAllTimeSpans4b()
   2: {
   3:     // erstmal die daten...
   4:     IEnumerable<Allocation> allocations = _context
   5:         .AllocationSet
   6:         .OfType<Allocation>();
   7:     // dann selektion und projektion...
   8:     DateTime currentDate = DateTime.Today;
   9:     IEnumerable<AllocationTimeSpan> allTimeSpans = allocations
  10:         .Where(a => a.AllocatedFrom <= currentDate && a.AllocatedTo >= currentDate)
  11:         .Select(a => new AllocationTimeSpan
  12:         {
  13:             From = a.AllocatedFrom,
  14:             To = a.AllocatedTo,
  15:             Status = (Status)(a.Status),
  16:         })
  17:         .Distinct()
  18:         .OrderBy(ts => ts.From)
  19:         .ToArray();
  20:     return allTimeSpans;
  21: }

Eine Zwischenvariable eingeführt, nicht weiter nachgedacht, und schon geht die Abfrage ungefiltert gegen die Datenbank! Ein dummer Flüchtigkeitsfehler mit womöglich weitreichenden Auswirkungen. Vom Schreibaufwand mal ganz abgesehen.

 

Ergo: Der Einsatz von var reduziert sowohl den initialen Schreibaufwand, als auch den Änderungsaufwand. Und er eliminiert Fehlerquellen.

Share |