A következő címkéjű bejegyzések mutatása: C#. Összes bejegyzés megjelenítése
A következő címkéjű bejegyzések mutatása: C#. Összes bejegyzés megjelenítése

2018. május 27., vasárnap

XAMARIN és a WCF

A ritka kivételektől eltekintve szinte mindegyik mobil alkalmazás kommunikál valamilyen backend rendszerrel, most csupán a Windows  Communication Foundation (WCF) vonalra térnék ki.

Sok helyen lehet találkozni olyannal, hogy nem működik a WCF Xamarin alatt, pedig csupán néhány korlátozással kell együtt élni a cél elérése érdekében.

Ezen a linken elérhető egy követhető leírás Xamarinhoz arról, hogyan kell összekötni egy WCF-es kiszolgálóval. Ehhez tennék kiegészítést néhány speciális esetet megvizsgálva.

Korlátozások

  • Nincs XAML konfig
A "hagyományos" XML alapú konfigurációs fájlokkal tényleg nem lehet megvalósítani, mivel nem lehet csak úgy odatenni az indítófájl mellé. Másfelől .NET C# alatt, - hasonlóan a WPF UI vezérlőkhöz - ami konfig fájlban XAML-lel leírható, az C# kóddal is. Ezt használja ki a fentebb linkelt tutoriál is.

  • Nincs NetTCPBinding
Fontos megjegyezni, hogy mobilon nem létezik a NetTCPBinding, csak valamilyen *HttpBinding típust lehet használni.

  • Nincs EndpointBehaviors
PCL készítésekor egy WCF kliensben a Behavior helyett az EndpointBehaviort kell használni, ami Windows RT-n még elfut, de Android és iOS rendszereken a MONO alatt NotImplementedExceptionnel elszáll. Szerencsére reflectionnel kinyerhető a megoldás:
//client.Endpoint.EndpointBehaviors.Add(new EndpointBehavior());

//client is of type System.ServiceModel.ClientBase<T>
var prop = client.Endpoint.GetType()
                 .GetTypeInfo()
                 .GetDeclaredProperty("Behaviors");
var obj = (KeyedCollection<Type, IEndpointBehavior>)prop
                 .GetValue(client.Endpoint);
obj.Add(new EndpointBehavior());

Ugyanez a helyzet a MessageInspectorral is:
//clientRuntime.ClientMessageInspectors.Add(new MessageInspector());

var prop = clientRuntime.GetType()
                        .GetTypeInfo()
                        .GetDeclaredProperty("MessageInspectors");
var obj = (ICollection<IClientMessageInspector>)prop
                        .GetValue(clientRuntime);
obj.Add(new MessageInspector());

Wrapper a WCF hívás körül

Korábban már írtam arról, hogyan célszerű felkészülni arra, ha a WCF kliens hívása kezeletlen hibára fut. Ilyenkor nem szabad a Close()-t ráhívni, mint ahogy azt teszi alapértelmezetten a USING, helyette Abort() kell.

Async/await és a WCF

Xamarin alatt is használhatók az async/await kulcsszavak, amik tökéletesen illeszkednek az aszinkron WCF hívások köré. Bővebb leírás itt található.

public class WcfServiceWrapper:IWcfServiceWrapper
{
    private IWcfService _wcfService;

    public WcfServiceWrapper(IWcfService wcfService)
    {
        if (wcfService == null) throw new ArgumentNullException("wcfService");

        _wcfService = wcfService;
    }

    public async Task<string> GetDataAsync(int number)
    {
        return await new TaskFactory()
          .FromAsync<int,string>(
            _wcfService.BeginGetData, 
            _wcfService.EndGetData, 
            number, 
            null, 
            TaskCreationOptions.None
          );
    }
}

.NET Portable Assembly


Ha esetleg bármi miatt szeretnénk hozzáadni mobilon is használható .NET portable assembly-t, akkor azok itt találhatók:
C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\

Generált proxy és az IExtensibleDataObject


A WCF által publikált WSDL alapján le lehet generálni a hálózaton keresztül utazó osztályokat. Amennyiben a DataContractSerializert használjuk szerver oldalon, akkor ráteszi az IExtensibleDataObject interfészt és a hozzátartozó ExtensionDataObject tulajdonságot ezen generált osztályokra. Önmagában nem lenne baj, hiszen a System.Runtime.Serialization.dll-ben elérhető ez az interfész és osztály, van belőle portable DLL is, gondolhatnánk, hogy majd hozzáadjuk a PCL projekthez és készen vagyunk. Nos, ez az út nem járható, nem engedi hozzáadni azzal az üzenettel, hogy már referálja a sajátját. A probléma az, hogy abban viszont nincsenek benne ezek. 

1) Egyik megoldás lehet, hogy a kiszolgáló oldalon a ServiceContract attribútumban vagy akár magában a DataContractSerializerben bekapcsoljuk, hogy IgnoreExtensionDataObject. Viszont ezek a nevüknek megfelelően csak annyit csinálnak, hogy figyelembe kell-e venni az ExtensionDataObjectet vagy sem, de továbbra is megmaradnak az osztályokban. Elvileg a platformspecifikus projekthez már hozzá lehet adni ezt a dll-t, és utána működhet valahogy a DependencyService-en keresztül, de ezt a vonalat nem próbáltam ki.

2) Ha biztosan tudjuk, hogy egyáltalán nem használjuk semmire az ExtensionDataObjectet, akkor egy-egy üres osztállyal és interfésszel könnyedén orvosolható a helyzet:
 public interface IExtensibleDataObject { ExtensionData { get; set; } }  public sealed class ExtensionDataObject {}

2018. január 5., péntek

Unity - NativePlugins visszaírja a beállításokat újraindításkor

Egyik cross platform fejlesztés során a NativePlugins nevű plugint kellett használnom néhány funkcióhoz, hogy azok a megszokott módon működjenek az adott operációs rendszeren. A plugin több mindenre is képes, amelyeket grafikus felületen lehet pipálgatva szabályozni, hogy mire van szükség. Nekem éppenséggel a megosztás kezelése és egy fájl kiválasztó modul kellett csak belőle:

Funkciók kiválasztása
A problémám a Unity első újraindítását követően jelentkezett, amikor Androidra akartam fordítani, de elpukkant, mondván, a Billing modul IInAppBillingService osztálya kétszer is hozzá van adva a projekthez. Másik árulkodó jel az volt, hogy újabb NPSettings.asset és EditorNotificationCenter.asset fájlok keletkeztek a fájlnevek végén növekvő számozással akárhányszor elindult a Unity és néha annak bezárása nélkül is. Megnézve az NP beállításait, szomorúan tapasztaltam, hogy minden visszaállt az alapértelmezett értékre: mindegyik funkció be volt pipálva és a Billing modul ütközött a már korábban használttal. 

Az NP szerkesztőfelületén az Inspectorból elérhető egy egészen mutatós leírás hozzá, de ilyen jelenségről nem írt, ami akár még abból is adódhatott, hogy a leírás korábbi verzióhoz tartozott.

Végül az NPSettings.cs fájlban meglett a titok, miszerint statikus konstruktorban szerepelt egy olyan kódrészlet, ami UnityEditorban mindig újragenerálta a beállításokat, ha egy bizonyos DISABLE_NPSETTINGS_GENERATION szimbólum nincs definiálva.

Ezt hozzáadva az Edit/Unity Project Settings/Player settings menüpont Scripting Define Symbols listájába, helyreállt a rend és többé nem generált új asset fájlokat és megtartott a beállításokat.
DISABLE_NPSETTINGS_GENERATION beállítása

2017. június 23., péntek

Entity Framework cache frissítése

Entity Framework 6-ban a DbContext automatikusan cache-eli az adatokat, ami az esetek jelentős részében teljesen jó dolognak számít. Azonban vannak olyan szituációk, amikor az elérni kívánt adatok az aktuális contexten kívül már megváltoztak (például másik felhasználó módosította vagy tárolt eljárás ért hozzá). 

Több lehetőség is létezik a helyzet kezelésére:
1. A követés kikapcsolása az AsNoTracking() utasítással
2. Régi DbContext példány eldobása és új létrehozása
3. ObjectQuery használata a MergeOptions beállításával a DBQuery helyett
4. Elem frissítése (Reload / Refresh)
5. Elem leválasztása (Detach)
6. GetDatabaseValues hívás, hogy egy adott Elem (Entity) frissítve legyen
7. A régi adatok használata

Ezen a linken részletesen körbejárják az egyes lehetőségeket, hogy melyiket mikor is éri meg használni. 

2016. szeptember 11., vasárnap

Nem publikus metódusok/osztályok elérése külön Unit test projektben

Jó ötlet és helyes dolog, ha a unit testek egy (vagy esetleg több) külön projektbe kerülnek, ezzel szeparálva az éles kódtól. Másfelől nézve viszont egy külső projektből hivatkozva alapjáraton csak a publikus dolgokat tudjuk elérni, így könnyen támadhatna az az ötlet (hibásan), hogy fájó szívvel megszegjük elveinket és publikussá tegyünk bizonyos belső adatszerkezeteket, metódusokat, változókat pusztán azért, hogy tesztelhessük azokat. 

A .NET C#-ban a láthatósági szintek között szerepel egy olyan, hogy internal,  ami azt jelenti, hogy csak az adott assembly-n belül lehetséges hivatkozni rá, csak azon belül látható. Amennyiben mégis megpróbálnánk kívülről elérni (például egy szeparált unit test projektből), akkor fordítási idejű hibát dobna, mondván, hogy nem fér hozzá.

Rossz megoldás: mindent publikussá
Egyik lehetséges megoldás erre, hogy ha a kérdéses osztályt, metódust, stb., publikussá tesszük. Ez maximum csak első látszatra tűnhet megoldásnak, mivel hamar előjön, hogy szinte mindent publikussá kéne tennünk, amivel pedig éppen csak az osztály egyik célját, a hozzáférés-szabályozást veszítjük el. Létezik ennél egy hatékonyabb megoldás is.

Helyes megoldás: assembly-k összebarátkoztatása (Friend Assemblies)
Egy barátnak jelölt assembly ugyanúgy hozzá tud férni egy másik assembly-ben lévő internal típusaihoz és egyéb tagjaihoz, mintha azok publikusak lennének. Az InternalsVisibleTo attribútum használatával lehet megjelölni egy vagy akár több assembly-t barátként.

Ezt az attribútumot annak a projektnek az AssemblyInfo.cs fájljába kell tenni, amelyiket szeretnénk elérhetővé tenni:

[assembly: InternalsVisibleTo("UnitTestProject")]

Ennek hatására projektszinten minden, ami internal láthatóságú, elérhetővé válik a barát számára.

2016. augusztus 31., szerda

RaisePropertyChanged attribútum property-re PostSharppal

MVVM patternt használva WPF fejlesztés során elkerülhetetlen, hogy implementáljuk az INotifyPropertyChanged interfészt. Amikor egy olyan tulajdonság változik a ViewModelben, amire a felületen valami rá van kötve, akkor egy PropertyChanged eseményt kell dobni, hogy értesüljön a UI is. Ehhez tartozóan egy property tipikusan így szokott kinézni:

public class MyViewModel : ViewModelBase
{
    private string demoProp;

    public string DemoProp
    {
        get { return demoProp; }
        set
        {
            if (demoProp == value) return;
            demoProp = value;
            OnPropertyChanged("DemoProp");
        }
    }
}

Ebben a példában a ViewModelBase ősosztály implementálja az interfészt:

public class ViewModelBase : INotifyPropertyChanged
{
    private event PropertyChangedEventHandler propertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        if (!string.IsNullOrWhiteSpace(propertyName) &&
           propertyChanged != null)
 {
       propertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }
    }
}



PostSharp nagy mértékben meg tudja könnyíteni az életet úgy, hogy átláthatóbb, karbantarthatóbb kódot eredményez az aspektus orientáltság révén. A fenti DemoProp kódját többféle módon is képes egyszerűsíteni, ebből két lehetséges:

1. Osztályszintű attribútum és nincs manuális kódolás
Ekkor elegendő egy attribútumot felhelyezni az osztályra és fordításkor minden publikus autoproperty megváltoztatásáról ugyanúgy értesülni fog a rendszer, mintha saját magunk implementáltuk volna az INotifyPropertyChanged interfészt, illetve kötöttük volna be a property-k set ágába az esemény kiváltását. Ennek részletes működéséről itt lehet olvasni.

Ennek a módszernek egyszerre előnye és hátránya is, hogy csak osztályra lehet feltenni ezt az attribútumot. Ebből adódóan nincs lehetőség arra, hogy ki lehessen választani, hogy melyik property-t figyelje és melyiket ne. 

2. Saját készítésű attribútum property-re
Előfordulnak olyan esetek, amikor nem jó az 1. módszer, mivel nem akarjuk az összes property-re ráakasztani. Például ha saját magunk már implementáltuk az INotifyPropertyChanged interfészt, viszont vannak olyan property-k, amikhez nem tartozik extra logika  az érték megváltozásához a felület értesítését leszámítva. Emiatt felesleges is lenne kézzel megcsinálni hozzá a backfieldet és a get/set metódusokat. Egyszerűbb lenne, ha csak egy attribútumot kéne feltenni egy autoproperty-re.

A fenti MyViewModel átírva így nézne ki. A különbség szerintem önmagáért beszél:

public class MyViewModel : ViewModelBase
{
    [RaisePropertyChanged]
    public string DemoProp get; set}
}


A példában használt RaisePropertyChanged PostSharp attribútum pedig alább látható:

using System;
using System.Reflection;
using PostSharp.Aspects;

[AttributeUsage(AttributeTargets.Property)]
[Serializable]
public class RaisePropertyChangedAttribute : OnMethodBoundaryAspect
{
    /// <summary>
    /// Property neve
    /// </summary>
    private string propertyName;
    /// <summary>
    /// történt-e változás, kell-e értesítés
    /// </summary>
    private bool hasChanged;

    /// <summary>
    /// fordítási időben lekérdezett mezőinformáció
    /// </summary>
    private PropertyInfo propertyInfo;

    public override void OnEntry(MethodExecutionArgs args)
    {
        //A CompileTimeValidate biztosítja, hogy ez a kódrészlet
        //csak ViewModelBase-ben lévő property-khez tartozó Sethez legyen hozzáfűzve

        //mivel ez egy Set metódusba való belépés elején fut, 
        //így biztosan van egy paramétere (value)

        object value = propertyInfo.GetValue(args.Instance);
        object param = args.Arguments[0];

        hasChanged = (value != null && param == null) ||
                        (value == null && param != null) ||
                        (value != null && !value.Equals(param));  

        if (!hasChanged)
        {
            args.FlowBehavior = FlowBehavior.Return;
            return;
        }

        base.OnEntry(args);
           
    }

    public override void OnSuccess(MethodExecutionArgs args)
    {
        //siker esetén kell csak dobni az értesítést, 
        //feltéve, hogy történt egyáltalán változás

        if (!hasChanged) return;

        ((ViewModelBase) args.Instance).ReportPropertyChanged(propertyName);
    }

    public override bool CompileTimeValidate(MethodBase method)
    {
        var type = method.ReflectedType;
        if (type == null) return false;

        //ha nem ő maga egy ViewModelBase és ráadásul nem is leszármazottja
        if (type != typeof(ViewModelBase) &&
            !type.IsSubclassOf(typeof(ViewModelBase))) return false;

        //ha nem Set metódus
        if (!IsPropertySetter(method)) return false;
           

        //fordítási időben eltároljuk a property nevét és a leíróját
        propertyName = GetPropertyName(method);
        propertyInfo = type.GetProperty(propertyName);
           
        return true;
    }

    private static string GetPropertyName(MethodBase method)
    {
        return method.Name.Replace("set_", "");
    }

    private static bool IsPropertySetter(MethodBase method)
    {
        return method.Name.StartsWith("set_");
    }
}

2016. február 17., szerda

Log4Net konfigurálása

Minden alkalmazás életében előbb vagy utóbb eljön a pillanat, hogy szükség lenne naplózásra azért, hogy beleláthassunk a nagy fekete doboz működésébe a debuggeren kívüli életben is. A Log4Net egy kiváló eszköz erre a célra, ráadásul a Nuget Package Managerrel könnyedén hozzá lehet adni az alkalmazásunkhoz. Eléggé részletes a hivatalos dokumentáció arról, hogy miként kell bekonfigurálni, viszont szerintem eleinte nehezen áll össze a teljes kép, pedig viszonylag egyszerűen be lehet üzemelni.

Jelen cikk keretében nem célom a különböző szűrők, naplózási szinten és formázások használatára kitérni, csak arra, hogy miként lehet szóra bírni.

Konfigurációs fájl

A Log4Nethez az úgynevezett appendereket és azok működését egy konfigurációs fájl írja le. Egyidejűleg akár több appendert is megadhatunk, a rendszer automatikusan kezeli, hogy mindegyik meghívódjon. Természetesen a szűrők és a naplózási szintek módosíthatják, hogy egy-egy naplózó utasítás esetén melyik appender fog ténylegesen írni is valamit.

Az alábbi konfig fájl példában a megadott két appenderrel egyidejűleg írunk fájlba óránkénti bontásban és a konzolra is:
 
<configuration>
  <log4net >
    <appender name="RollingFileAppender" 
              type="log4net.Appender.RollingFileAppender">
      <file value="log\dm-info" />
      <param name="DatePattern" value=".yyyy.MM.dd-HH'.log'"/>
      <appendToFile value="true" />
      <rollingStyle value="Date" />
      <maxSizeRollBackups value="1" />
      <staticLogFileName value="false" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date | %-5level | thread:%thread | %message%newline" />
      </layout>
    </appender>
  
    <appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
      <target value="Console.Out" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date | %-5level | thread:%thread | %message%newline" />
      </layout>
      <filter type="log4net.Filter.LevelRangeFilter">
        <param name="LevelMin" value="DEBUG"/>
      </filter>
    </appender>

    <root>
      <level value="INFO" />
      <appender-ref ref="ConsoleAppender" />
      <appender-ref ref="RollingFileAppender" />
    </root>
  </log4net>
</configuration>

Fontos kiemelni, hogy a konfigfájlon legyen beállítva a Copy to output directory tulajdonság valamelyik Copy lehetőségre, ellenkező esetben nem fog csinálni semmit sem, viszont hibát sem fog dobni. 

Inicializálás

Ahhoz, hogy egyáltalán tudjon róla, hogy milyen appenderek vannak beállítva, illetve melyik fájl tartalmazza a konfigot, az XmlConfiguratorral fel kell dolgozni a megadott konfig fájlt. Ez többféleképpen is történhet. Például meghívhatjuk az alkalmazás belépési pontjánál. Ezzel az a baj, hogy ha ki van emelve egy olyan közös projektbe a naplózás, amit több alkalmazás is használ, akkor mindegyik alkalmazásban külön szükséges hozzáadni ezt az utasítást:

log4net.Config.XmlConfigurator.Configure(
                               new System.IO.FileInfo(@"Path\To\Log4net.config"));

Ennél szerintem szebb és egyszerűbb megoldás, hogy ha a naplózást tartalmazó projekt Properties/AssemblyInfo.cs fájlba hozzáadjuk az alábbi utasítást. A két paramétere megadja, hogy melyik konfigurációs fájlt szeretnénk betölteni hozzá, illetve hogy kell-e frissíteni a beállításokat, hogy ha megváltozott a konfigurációs fájl tartalma. Ezzel a módszerrel a hivatkozó projektekben, alkalmazásokban később már nem kell újra és újra meghívni az XmlConfiguratort.

[assembly: log4net.Config.XmlConfigurator(ConfigFile = "Path\To\Log4Net.config", Watch = true)]

Logger osztály és interface

Miután megvagyunk a konfigurációs fájllal és annak beolvastatásával, jöhet a tényleges logger implementálása. Önmagában a log4net használatához elegendő lenne az alábbi kódrészlet, amivel a log változó metódusaival már lehetne naplózni sorainkat:

private static readonly log4net.ILog log = 
                        log4net.LogManager.GetLogger(typeof(Logger));

Másfelől lehet ezt még csinosítani egy wrapper osztállyal, ami már tetszőleges paraméterezésű metódusokat tartalmazhat kifelé. Az ILogger interface-re azért lehet szükség, mert így a későbbiekben egyéb naplózó logikát lehet implementálni és transzparensen használni.

public class Logger : ILogger
{
    private static readonly log4net.ILog log = 
                            log4net.LogManager.GetLogger(typeof(Logger));

    #region ILogger Members

    public void LogException(Exception exception, string message)
    {
        if (log.IsErrorEnabled)
           log.Error(
                string.Format(CultureInfo.InvariantCulture, "{0}", message), 
                exception);
    }
       
    public void LogException(Exception exception)
    {
        LogException(exception, exception.Message);
    }

    public void LogError(string message)
    {
        if (log.IsErrorEnabled)
            log.Error(string.Format(CultureInfo.InvariantCulture, "{0}", message));
    }

    public void LogWarningMessage(string message)
    {
        if (log.IsWarnEnabled)
            log.Warn(string.Format(CultureInfo.InvariantCulture, "{0}", message));
    }

    public void LogInfoMessage(string message)
    {
        if (log.IsInfoEnabled)
            log.Info(string.Format(CultureInfo.InvariantCulture, "{0}", message));
    }
    
    public void LogDebug(string message)
    {
        if (log.IsDebugEnabled)
            log.Debug(string.Format(CultureInfo.InvariantCulture, "{0}", message));
    }
       
    #endregion
}

A hozzátartozó ILogger interface:
public interface ILogger
{
    void LogException(Exception exception);
    void LogException(Exception exception, string customMessage);
    void LogError(string message);
    void LogDebug(string message);
    void LogWarningMessage(string message);
    void LogInfoMessage(string message);
}

LogHelper osztály

Jó lenne, ha elkerülhetnénk, hogy minden egyes alkalommal, ahol használjuk, példányosítani kelljen a Loggert. A metódus statikussá tétele nem megoldás, mivel az interface-t implementáló metódus nem lehet statikus. Egy lehetséges megoldás a problémára, hogy ha becsomagoljuk egy újabb wrapper osztályba, aminek a statikus metódusai az egyetlen Logger példány metódusait hívják.

public static class LogHelper
{
    private static readonly ILogger logger = new Logger();
       
    public static void LogInfo(string message)
    {
        logger.LogInfoMessage(message);
    }

    public static void LogInfo(string pattern, params object[] args)
    {
        LogInfo(string.Format(CultureInfo.InvariantCulture, pattern, args));
    }
       
    public static void LogDebug(string message)
    {
        logger.LogDebug(message);
    }

    public static void LogDebug(string pattern, params object[] args)
    {
        LogDebug(string.Format(CultureInfo.InvariantCulture, pattern, args));
    }

    public static void LogError(string message)
    {
        logger.LogError(message);
    }

    public static void LogError(string pattern, params object[] args)
    {
        LogError(string.Format(CultureInfo.InvariantCulture, pattern, args));
    }

    public static void LogException(Exception exception, string message)
    {
        logger.LogException(exception, message);
    }

    public static void LogException(
         Exception exception, string pattern, params object[] args)
    {
        string message = string.Format(CultureInfo.InvariantCulture, pattern, args);
        LogException(exception, message);
    }

    public static void LogException(Exception ex)
    {
        logger.LogException(ex);
    }
       
    public static void SetThreadVariable(string key, string variable)
    {
        log4net.ThreadContext.Properties[key] = variable;
    }

    public static void SetGlobalVariable(string key, string variable)
    {
        log4net.GlobalContext.Properties[key] = variable;
    }
}


Összegzés

Ezek után nem maradt más hátra, mint hogy használjuk a LogHelper statikus metódusait és élvezzük, ahogy termeli a bejegyzéseket a beállított helyen és formában.