Renvoyer du XML à partir de l'action d'un contrôleur en tant qu'ActionResult?

139

Quelle est la meilleure façon de renvoyer du XML à partir de l'action d'un contrôleur dans ASP.NET MVC? Il existe une bonne façon de renvoyer JSON, mais pas pour XML. Ai-je vraiment besoin d'acheminer le XML via une vue, ou devrais-je utiliser la méthode de réponse qui n'est pas la meilleure pratique.

Ken Randall
la source

Réponses:

114

Utilisez l'action XmlResult de MVCContrib.

Pour référence, voici leur code:

public class XmlResult : ActionResult
{
    private object objectToSerialize;

    /// <summary>
    /// Initializes a new instance of the <see cref="XmlResult"/> class.
    /// </summary>
    /// <param name="objectToSerialize">The object to serialize to XML.</param>
    public XmlResult(object objectToSerialize)
    {
        this.objectToSerialize = objectToSerialize;
    }

    /// <summary>
    /// Gets the object to be serialized to XML.
    /// </summary>
    public object ObjectToSerialize
    {
        get { return this.objectToSerialize; }
    }

    /// <summary>
    /// Serialises the object that was passed into the constructor to XML and writes the corresponding XML to the result stream.
    /// </summary>
    /// <param name="context">The controller context for the current request.</param>
    public override void ExecuteResult(ControllerContext context)
    {
        if (this.objectToSerialize != null)
        {
            context.HttpContext.Response.Clear();
            var xs = new System.Xml.Serialization.XmlSerializer(this.objectToSerialize.GetType());
            context.HttpContext.Response.ContentType = "text/xml";
            xs.Serialize(context.HttpContext.Response.Output, this.objectToSerialize);
        }
    }
}
Luke Smith
la source
12
La classe ici est tirée directement du projet MVC Contrib. Je ne sais pas si c'est ce qui est qualifié de rouler le vôtre.
Sailing Judo
3
Où mettriez-vous cette classe si vous suivez la convention ASP.NET MVC? Dossier des contrôleurs? Au même endroit où vous placeriez vos ViewModels, peut-être?
p.campbell
7
@pcampbel, je préfère créer des dossiers séparés dans ma racine du projet pour tous les types de classes: résultats, filtres, routage, etc.
Anthony Serdioukov
L'utilisation des XmlSerialiserannotations et des membres peut être difficile à gérer. Depuis que Luke a publié cette réponse (il y a environ quatre ans), Linq to XML s'est avéré être un remplacement plus élégant et plus puissant pour la plupart des scénarios courants. Consultez ma réponse pour un exemple de la façon de procéder.
Drew Noakes
133
return this.Content(xmlString, "text/xml");
Petr
la source
1
Wow, cela m'a vraiment aidé, mais je commence seulement à bricoler le truc MVC.
Denis Valeev
Si vous travaillez avec Linq to XML, créer une forme de chaîne du document est un gaspillage - il est préférable de travailler avec des flux .
Drew Noakes
2
@Drew Noakes: Non, ce n'est pas le cas. Si vous écrivez directement dans le flux HttpContext.Response.Output, vous obtiendrez un YSOD sur les serveurs basés sur WinXP. Il semble être corrigé sur Vista +, ce qui est particulièrement problématique si vous développez sur Windows 7 et déployez sur Windows XP (Server 2003?). Si vous le faites, vous devez d'abord écrire dans un flux mémoire, puis copier le flux mémoire dans le flux de sortie ...
Stefan Steiger
6
@Quandary, ok, je vais répéter le point: créer des chaînes est un gaspillage lorsque vous pouvez éviter l'allocation / la collecte / les exceptions de mémoire insuffisante en utilisant des flux, à moins que vous ne travailliez sur des systèmes informatiques vieux de 11 ans qui présentent une erreur.
Drew Noakes
1
Vous voudrez peut-être utiliser le application/xmltype MIME à la place.
Fred le
32

Si vous construisez le XML à l'aide de l'excellent framework Linq-to-XML, cette approche sera utile.

Je crée une XDocumentméthode dans l'action.

public ActionResult MyXmlAction()
{
    // Create your own XDocument according to your requirements
    var xml = new XDocument(
        new XElement("root",
            new XAttribute("version", "2.0"),
            new XElement("child", "Hello World!")));

    return new XmlActionResult(xml);
}

Cette personnalisation réutilisable ActionResultsérialise le XML pour vous.

public sealed class XmlActionResult : ActionResult
{
    private readonly XDocument _document;

    public Formatting Formatting { get; set; }
    public string MimeType { get; set; }

    public XmlActionResult(XDocument document)
    {
        if (document == null)
            throw new ArgumentNullException("document");

        _document = document;

        // Default values
        MimeType = "text/xml";
        Formatting = Formatting.None;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.Clear();
        context.HttpContext.Response.ContentType = MimeType;

        using (var writer = new XmlTextWriter(context.HttpContext.Response.OutputStream, Encoding.UTF8) { Formatting = Formatting })
            _document.WriteTo(writer);
    }
}

Vous pouvez spécifier un type MIME (tel que application/rss+xml) et si la sortie doit être indentée si nécessaire. Les deux propriétés ont des valeurs par défaut sensibles.

Si vous avez besoin d'un encodage autre que UTF8, alors il est simple d'ajouter une propriété pour cela aussi.

Drew Noakes
la source
Pensez-vous qu'il est possible de modifier cela pour une utilisation dans un contrôleur d'API?
Ray Ackley
@RayAckley, je ne sais pas car je n'ai pas encore essayé les nouvelles fonctionnalités de l'API Web. Si vous découvrez, faites-le nous savoir.
Drew Noakes
Je pense que j'étais sur la mauvaise voie avec la question du contrôleur API (je ne fais normalement pas de trucs MVC). Je viens de l'implémenter en tant que contrôleur régulier et cela a très bien fonctionné.
Ray Ackley
Excellent travail Drew. J'utilise une version de votre XmlActionResult pour mes besoins. Mon environnement de développement: ASP.NET 4 MVC J'appelle la méthode de mon contrôleur (retourne XmlActionResult - contenant un XML transformé pour MS-Excel) depuis ajax. La fonction Ajax Success a un paramètre de données qui contient le xml transformé. Comment utiliser ce paramètre de données pour lancer une fenêtre de navigateur et afficher une boîte de dialogue Enregistrer sous ou simplement ouvrir Excel?
sheir
@sheir, si vous voulez que le navigateur lance le fichier, vous ne devez pas le charger via AJAX. Accédez directement à votre méthode d'action. Le type MIME déterminera la manière dont il est géré par le navigateur. Utiliser quelque chose comme application/octet-streampour forcer le téléchargement. Je ne sais pas quel type MIME lance Excel, mais vous devriez pouvoir le trouver en ligne assez facilement.
Drew Noakes
26

Si vous souhaitez uniquement renvoyer du xml via une requête et que vous avez votre "bloc" xml, vous pouvez simplement faire (en tant qu'action dans votre contrôleur):

public string Xml()
{
    Response.ContentType = "text/xml";
    return yourXmlChunk;
}
Erik
la source
4

J'ai dû le faire récemment pour un projet Sitecore qui utilise une méthode pour créer un XmlDocument à partir d'un élément Sitecore et de ses enfants et le renvoie du contrôleur ActionResult sous forme de fichier. Ma solution:

public virtual ActionResult ReturnXml()
{
    return File(Encoding.UTF8.GetBytes(GenerateXmlFeed().OuterXml), "text/xml");
}
Matthew Price
la source
2

Enfin réussir à obtenir ce travail et j'ai pensé que je documenterais comment ici dans l'espoir de sauver les autres de la douleur.

Environnement

  • VS2012
  • SQL Server 2008R2
  • .NET 4.5
  • ASP.NET MVC4 (rasoir)
  • Windows 7

Navigateurs Web pris en charge

  • Renard de feu 23
  • IE 10
  • Chrome 29
  • Opéra 16
  • Safari 5.1.7 (le dernier pour Windows?)

Ma tâche consistait à cliquer sur un bouton d'interface utilisateur, à appeler une méthode sur mon contrôleur (avec quelques paramètres), puis à lui faire renvoyer un XML MS-Excel via une transformation xslt. Le XML MS-Excel renvoyé amènerait alors le navigateur à faire apparaître la boîte de dialogue Ouvrir / Enregistrer. Cela devait fonctionner dans tous les navigateurs (listés ci-dessus).

Au début, j'ai essayé avec Ajax et de créer une ancre dynamique avec l'attribut "download" pour le nom de fichier, mais cela ne fonctionnait que pour environ 3 des 5 navigateurs (FF, Chrome, Opera) et non pour IE ou Safari. Et il y avait des problèmes avec la tentative de déclencher par programme l'événement Click de l'ancre pour provoquer le "téléchargement" réel.

Ce que j'ai fini par faire était d'utiliser un IFRAME "invisible" et cela a fonctionné pour les 5 navigateurs!

Voici donc ce que j'ai proposé: [veuillez noter que je ne suis en aucun cas un gourou html / javascript et que je n'ai inclus que le code pertinent]

HTML (extrait de bits pertinents)

<div id="docxOutput">
<iframe id="ifOffice" name="ifOffice" width="0" height="0"
    hidden="hidden" seamless='seamless' frameBorder="0" scrolling="no"></iframe></div>

JAVASCRIPT

//url to call in the controller to get MS-Excel xml
var _lnkToControllerExcel = '@Url.Action("ExportToExcel", "Home")';
$("#btExportToExcel").on("click", function (event) {
    event.preventDefault();

    $("#ProgressDialog").show();//like an ajax loader gif

    //grab the basket as xml                
    var keys = GetMyKeys();//returns delimited list of keys (for selected items from UI) 

    //potential problem - the querystring might be too long??
    //2K in IE8
    //4096 characters in ASP.Net
    //parameter key names must match signature of Controller method
    var qsParams = [
    'keys=' + keys,
    'locale=' + '@locale'               
    ].join('&');

    //The element with id="ifOffice"
    var officeFrame = $("#ifOffice")[0];

    //construct the url for the iframe
    var srcUrl = _lnkToControllerExcel + '?' + qsParams;

    try {
        if (officeFrame != null) {
            //Controller method can take up to 4 seconds to return
            officeFrame.setAttribute("src", srcUrl);
        }
        else {
            alert('ExportToExcel - failed to get reference to the office iframe!');
        }
    } catch (ex) {
        var errMsg = "ExportToExcel Button Click Handler Error: ";
        HandleException(ex, errMsg);
    }
    finally {
        //Need a small 3 second ( delay for the generated MS-Excel XML to come down from server)
        setTimeout(function () {
            //after the timeout then hide the loader graphic
            $("#ProgressDialog").hide();
        }, 3000);

        //clean up
        officeFrame = null;
        srcUrl = null;
        qsParams = null;
        keys = null;
    }
});

C # SERVER-SIDE (extrait de code) @Drew a créé un ActionResult personnalisé appelé XmlActionResult que j'ai modifié à mes fins.

Renvoyer du XML à partir de l'action d'un contrôleur en tant qu'ActionResult?

Ma méthode Controller (retourne ActionResult)

  • transmet le paramètre keys à un proc stocké SQL Server qui génère un XML
  • ce XML est ensuite transformé via xslt en un XML MS-Excel (XmlDocument)
  • crée une instance du XmlActionResult modifié et la renvoie

    XmlActionResult result = new XmlActionResult (excelXML, "application / vnd.ms-excel"); version de la chaîne = DateTime.Now.ToString ("jj_MMM_aaaa_hhmmsstt"); string fileMask = "LabelExport_ {0} .xml";
    result.DownloadFilename = string.Format (fileMask, version); résultat de retour;

La principale modification de la classe XmlActionResult créée par @Drew.

public override void ExecuteResult(ControllerContext context)
{
    string lastModDate = DateTime.Now.ToString("R");

    //Content-Disposition: attachment; filename="<file name.xml>" 
    // must set the Content-Disposition so that the web browser will pop the open/save dialog
    string disposition = "attachment; " +
                        "filename=\"" + this.DownloadFilename + "\"; ";

    context.HttpContext.Response.Clear();
    context.HttpContext.Response.ClearContent();
    context.HttpContext.Response.ClearHeaders();
    context.HttpContext.Response.Cookies.Clear();
    context.HttpContext.Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);// Stop Caching in IE
    context.HttpContext.Response.Cache.SetNoStore();// Stop Caching in Firefox
    context.HttpContext.Response.Cache.SetMaxAge(TimeSpan.Zero);
    context.HttpContext.Response.CacheControl = "private";
    context.HttpContext.Response.Cache.SetLastModified(DateTime.Now.ToUniversalTime());
    context.HttpContext.Response.ContentType = this.MimeType;
    context.HttpContext.Response.Charset = System.Text.UTF8Encoding.UTF8.WebName;

    //context.HttpContext.Response.Headers.Add("name", "value");
    context.HttpContext.Response.Headers.Add("Last-Modified", lastModDate);
    context.HttpContext.Response.Headers.Add("Pragma", "no-cache"); // HTTP 1.0.
    context.HttpContext.Response.Headers.Add("Expires", "0"); // Proxies.

    context.HttpContext.Response.AppendHeader("Content-Disposition", disposition);

    using (var writer = new XmlTextWriter(context.HttpContext.Response.OutputStream, this.Encoding)
    { Formatting = this.Formatting })
        this.Document.WriteTo(writer);
}

C'était essentiellement ça. J'espère que cela aide les autres.

héritier
la source
1

Une option simple qui vous permettra d'utiliser les flux et tout ce qui est return File(stream, "text/xml");.

Casey
la source
0

Voici une façon simple de le faire:

        var xml = new XDocument(
            new XElement("root",
            new XAttribute("version", "2.0"),
            new XElement("child", "Hello World!")));
        MemoryStream ms = new MemoryStream();
        xml.Save(ms);
        return File(new MemoryStream(ms.ToArray()), "text/xml", "HelloWorld.xml");
user2670714
la source
Pourquoi cela crée-t-il deux flux de mémoire? Pourquoi ne pas simplement passer msdirectement, au lieu de le copier dans un nouveau? Les deux objets auront la même durée de vie.
jpaugh
Faites un ms.Position=0et vous pouvez renvoyer le flux de mémoire d'origine. Alors vous pouvezreturn new FileStreamResult(ms,"text/xml");
Carter Medlin
0

Une petite variation de la réponse de Drew Noakes qui utilise la méthode Save () de XDocument.

public sealed class XmlActionResult : ActionResult
{
    private readonly XDocument _document;
    public string MimeType { get; set; }

    public XmlActionResult(XDocument document)
    {
        if (document == null)
            throw new ArgumentNullException("document");

        _document = document;

        // Default values
        MimeType = "text/xml";
    }

    public override void ExecuteResult(ControllerContext context)
    {
        context.HttpContext.Response.Clear();
        context.HttpContext.Response.ContentType = MimeType;
        _document.Save(context.HttpContext.Response.OutputStream)
    }
}
Nelson Lopez Centeno
la source