code it

Martins Tech Blog

noHistory in der Activity und der Seiteneffekt

Normalerweise kann man im savedInstanceState Statusinformationen ablegen, die dann beim Resume der Activity dafür sorgen, dass die Benutzeroberfläche wieder so hergestellt werden kann, wie sie war bevor die App in den Ruhemodus versetzt wurde.

Heute hatte ich das Phänomen, dass im onSaveInstanceState meine Daten in das Bundle geschrieben wurden, beim Fortsetzen der App im onCreate aber immer NULL als savedInstanceState bereitstand, was dafür sorgte, dass sich die Benutzeroberfläche immer zurücksetzte auf den Initialzustand.

Nach einiger Suche kam ich dann auf des Problems Lösung: Ich hatte in der Manifest-Datei der Activity das Flag noHistory="true" gesetzt. Laut Dokumentation hat dies folgenden Effekt:
Whether or not the activity should be removed from the activity stack and finished [...] when the user navigates away from it and it's no longer visible on screen.

[...] 

A value of "true" means that the activity will not leave a historical trace. It will not remain in the activity stack for the task, so the user will not be able to return to it.
Mein Verständnis dieses Flags war, dass dadurch die Activity nicht auf dem Backstack landet. Das funktioniert auch. Allerdings meint "the user will not be able to return to it" auch, dass sämtliche Informationen im Bundle verworfen werden und jedes Resume wie ein Neustart der Activity ist.

Attribute in Metadaten prüfen

Heute wurde mir eine gute Frage zu einem Problem gestellt, das ich bisher als trivial abgetan habe: Wie ermittle ich in ASP.NET für eine Eigenschaft des Models, ob dort ein bestimmtes Attribut gesetzt ist? Solche Attribute (insbesondere die vom Typ ValidationAttribute) werden ja verwendet, um Eingabevalidierung vorzunehmen.

Einstiegspunkt soll eine ExtensionMethod auf HtmlHelper sein, wie man sie in ASP.NET häufig findet und die einfach nur einen Text in der Html-Seite ausgeben soll.
public static MvcHtmlString RequiredMark<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
    var required = expression.IsRequired();
    return new MvcHtmlString((!required ? "kein " : string.Empty) + "Pflichtfeld");
}
Spannender Teil daran ist jetzt die schon verwendete ExtensionMethod IsRequired, die für die eigentliche Magie zuständig ist. Diese ist auch noch recht trivial: Sie prüft die Eingabe und ruft die noch zu erstellende Methode HasAttribute auf, die dann die eigentliche Prüfung auf das gesuchte Attribute durchführt:
private static bool IsRequired<T, V>(this Expression<Func<T, V>> expression)
{
    var memberExpression = expression.Body as MemberExpression;
    if (memberExpression == null)
        throw new InvalidOperationException("Expression must be a member expression");

    return HasAttribute(memberExpression.Member, typeof(RequiredAttribute));
}
Auch diese Methode ist recht einfach implementiert - schließlich gibt es in der Klasse Attribute eine statische Methode IsDefined, die dafür verwendet werden kann:
private static bool HasAttribute([NotNull] MemberInfo memberInfo, [NotNull] Type attributeType)
{
    if (memberInfo == null)
    {
        throw new ArgumentNullException();
    }

    var isDefinedOnMember = Attribute.IsDefined(memberInfo, attributeType);

    return isDefinedOnMember;
}
Funktioniert super ..... Funktioniert super und deswegen hören hier die meisten Lösungsvorschläge in einschlägigen Foren auch schon auf.... Funktioniert super, so lange man das Attribut direkt auf dem Model definiert. Allerdings gibt es auch die Möglichkeit, Metadatentypen zu definieren, die dann die Attribute beinhalten. Das ist immer dann sinnvoll, wenn das eigentliche Modell automatisch generiert wird - aus einem Designer oder aus einem T4-Template.

Schauen wir uns ein Beispiel an:
public class MyViewModel
{
    [Required]
    public string Name { get; set; }
}
Dafür funktioniert die bisher erstellte Lösung. Aber bei dem folgenden Beispiel schlägt unsere Prüfung fehl:
[MetadataType(typeof(MyViewModelMetaData))]
public class MyViewModel
{
    public string Name { get; set; }
}

public class MyViewModelMetaData
{
    [Required]
    public string Name { get; set; }
}

Wie kann das Problem gelöst werden? Ganz einfach - zusätzlich zu der schon erstellten Prüfung auf direkt gesetzte Attribute muss nun noch dem Metadatenattribut auf dem Typ gefolgt werden und in diesem Typ geprüft werden welche Attribute auf dem gleichnamigen Member gesetzt sind:
private static bool HasAttribute([NotNull] MemberInfo memberInfo, [NotNull] Type attributeType)
{
    if (memberInfo == null)
    {
        throw new ArgumentNullException();
    }

    // hier prüfe ich direkt gesetzte Attribute
    var isDefinedOnMember = Attribute.IsDefined(memberInfo, attributeType);
    if (isDefinedOnMember)
    {
        return true;
    }

    // jetzt wird noch der Metadatentyp geprüft
    var type = GetMetadataType(memberInfo);
    if (type == null)
    {
        return false;
    }

           
    return type.GetProperties().Any(prop => prop.Name.Equals(memberInfo.Name, StringComparison.OrdinalIgnoreCase) && Attribute.IsDefined(prop, attributeType));
}
Nun liefert die erstellte Methode auch in diesem Fall korrekte Ergebnisse.

Happy coding.

Constraints für WebAPI-Parameter

Seit die WebAPI in der Version 2 vorliegt, kann man dort nicht nur basierend auf Konventionen agieren, sondern die Routen auch mit Hilfe der Attribute RoutePrefix und Route definieren. Schauen wir uns ein Beispiel an:

[RoutePrefix("api/v1/books")]
public class BooksController : ApiController {

    [HttpGet]
    [Route("{id}")]
    public IHttpActionResult GetItem(int id)
    {
        // in production mode we would search for the book in database
        if (id == 87653)
        {
            var book = new Book
            {
                Author = "John Grisham",
                Title = "Gray Mountain",
                Id = 87653,
                Isbn = "978-0385537148"
            };
            return Ok(book);
        }

        return NotFound();
    }
}

Aufrufbar ist das Ganze nun über die Route /api/books/87653.


In der Action habe ich definiert, dass die Id vom Datentyp int sein soll. Was passiert, wenn hier kein int übergeben wird, also wenn hier z.B. die ISBN übergeben wird?


ASP.NET kann den übergebenen Wert nicht mappen und unser Parameter ist nicht Nullable. Deswegen wird der Request mit dem Statuscode 400 abgewiesen.

WebAPI unterstützt auch sogenannte Constraints. Damit kann näher spezifiziert werden, welche Bedingungen die Parameter erfüllen müssen. In meinem Fall ist der Parameter id ein int. Das definiere ich nun zusätzlich in der Route.
[HttpGet]
[Route("{id:int}")]
public IHttpActionResult GetItem(int id)
{
    // do all the fancy stuff
}
Was passiert nun, wenn ein Request mit der ISBN ankommt?


Der Request wird noch immer abgewiesen, aber dieses mal nicht mit Statuscode 400 sondern mit dem Statuscode 404, denn nun wird bereits bei der Auswertung der Routentabelle erkannt, dass keine Route vorliegt, die für diesen Request passt.

Solche Constraints können auch kombiniert werden. Dazu verbindet man verschiedene Constraints einfach mit einem Doppelpunkt. 

[HttpGet]
[Route("{id:int:min(1):max(100000)}")]
public IHttpActionResult GetItem(int id)
{
    // do all the fancy stuff
}

Eine detaillierte Liste der möglichen Constraints können in der MSDN nachgelesen werden.

Wem die möglichen Constraints nicht ausreichen, dem steht es frei, eigene Constraints zu schreiben. Ich möchte das am Beispiel eines ISBN13-Constraints machen. Das Vorgehen ist ganz einfach. 

Zunächst erstellt man eine neue Klasse die das Interface IHttpRouteConstraint implementiert:
public class Isbn13Constraint : IHttpRouteConstraint
{
    private static bool IsIsbn13Valid(string inputValue) { 
        // remove delimiters 
        var isbn = Regex.Replace(inputValue, @"-|\.| ", ""); 

        // validate number of digits 
        if (isbn.Length != 13)
        {
            return false;
        }
        // calculate the product of the digit multiplication operations 
        var product = 0;
        for (var i = 0; i < (isbn.Length - 1); i++)
        {
            product += Convert.ToInt16(isbn.Substring(i, 1)) * (1 + (2 * (i % 2)));
        } 

        // calculate the check digit 
        var  checkdigit = Convert.ToString(10 - (product % 10)); 

        // validate check digit 
        return (checkdigit == isbn.Substring(isbn.Length - 1, 1)); 
    } 

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
        IDictionary values, HttpRouteDirection routeDirection)
    {
        object value;
        if (!values.TryGetValue(parameterName, out value) || value == null)
        {
            return false;
        }

        var stringValue = value as String;
        return stringValue != null && IsIsbn13Valid(stringValue);
    }
}

Der Rückgabewert der einzige Methode Match ist selbsterklärend - true wenn alles passt, false wenn nicht. Der hier eben erzeigte Constraint übernimmt gleich noch die Validierung der Gültigkeit der ISBN. Zugegeben - man kann sich darüber streiten, ob das in das Aufgabengebiet eines Constraints fällt. Schon allein wegen des Rückgabecodes "Not Found" statt "Bad Request" würde ich diese Prüfung eher woanders ansiedeln.

Dieser neue Constraint muss nun in der WebApiConfig noch registriert werden. Statt MapHttpAttributeRoutes ohne Parameter aufzurufen, übergeben wir hier einen neuen ConstraintResolver, der den eben erstellten Constraint enthält.

var constraintResolver = new DefaultInlineConstraintResolver();
constraintResolver.ConstraintMap.Add("isbn13", typeof(Isbn13Constraint));

config.MapHttpAttributeRoutes(constraintResolver);
Das war's auch schon fast. Der Constraint kann nun ebenso wie auch die anderen Constraints verwendet werden.

[HttpGet]
[Route("{isbn:isbn13}")]
public IHttpActionResult GetItem(string isbn)
{
    // do all the fancy stuff
}
Happy coding.