¿Devolver XML de la acción de un controlador como un ActionResult?

139

¿Cuál es la mejor manera de devolver XML de la acción de un controlador en ASP.NET MVC? Hay una buena manera de devolver JSON, pero no para XML. ¿Realmente necesito enrutar el XML a través de una vista, o debo hacer la forma de respuesta que no es la mejor práctica.

Ken Randall
fuente

Respuestas:

114

Use MVCContrib 's XmlResult Acción.

Para referencia aquí está su código:

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
fuente
12
La clase aquí se toma directamente del proyecto MVC Contrib. No estoy seguro si eso es lo que califica como rodar el tuyo.
Vela Judo
3
¿Dónde colocarías esta clase si sigues la convención ASP.NET MVC? Carpeta de controladores? ¿Quizás en el mismo lugar donde colocarías tus ViewModels?
p.campbell
77
@pcampbel, prefiero crear carpetas separadas en la raíz de mi proyecto para todo tipo de clases: resultados, filtros, enrutamiento, etc.
Anthony Serdyukov
El uso XmlSerialisery las anotaciones de miembros pueden ser difíciles de mantener. Desde que Luke publicó esta respuesta (hace unos cuatro años), Linq to XML ha demostrado ser un reemplazo más elegante y poderoso para los escenarios más comunes. Mira mi respuesta para ver un ejemplo de cómo hacer esto.
Drew Noakes
133
return this.Content(xmlString, "text/xml");
Petr
fuente
1
Wow, esto realmente me ayudó, pero solo estoy empezando a jugar con el MVC.
Denis Valeev
Si está trabajando con Linq to XML, crear una forma de cadena del documento es un desperdicio; es mejor trabajar con secuencias .
Drew Noakes
2
@Drew Noakes: No, no lo es. Si escribe directamente en la secuencia HttpContext.Response.Output, obtendrá un YSOD en servidores basados ​​en WinXP. Parece estar arreglado en Vista +, lo cual es especialmente problemático si desarrolla en Windows 7 y lo implementa en Windows XP (Server 2003?). Si lo hace, primero debe escribir en una secuencia de memoria y luego copiar la secuencia de memoria en la secuencia de salida ...
Stefan Steiger
66
@Quandary, ok. Replantearé el punto: crear cadenas es un desperdicio cuando podría evitar la asignación / recopilación / excepciones de falta de memoria mediante el uso de secuencias, a menos que esté trabajando en sistemas informáticos de 11 años que muestran un error.
Drew Noakes
1
Es posible que desee utilizar el tipo application/xmlMIME en su lugar.
Fred
32

Si está creando el XML utilizando el excelente marco de trabajo de Linq a XML, este enfoque será útil.

Creo un XDocumenten el método de acción.

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);
}

Esta serie reutilizable y personalizada ActionResultserializa el XML por usted.

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);
    }
}

Puede especificar un tipo MIME (como application/rss+xml) y si la salida debe sangrarse si es necesario. Ambas propiedades tienen valores predeterminados razonables.

Si necesita una codificación que no sea UTF8, entonces también es simple agregar una propiedad para eso.

Drew Noakes
fuente
¿Crees que es posible modificar esto para usarlo en un controlador API?
Ray Ackley el
@RayAckley, no sé, ya que aún no he probado las nuevas cosas de la API web. Si se entera, háganoslo saber.
Drew Noakes el
Creo que estaba en el camino equivocado con la pregunta del controlador API (normalmente no hago cosas MVC). Lo acabo de implementar como un controlador normal y funcionó muy bien.
Ray Ackley el
Buen trabajo Drew. Estoy usando un sabor de su XmlActionResult para mi requerimiento. Mi entorno de desarrollo: ASP.NET 4 MVC Llamo al método de mi controlador (devuelve XmlActionResult - que contiene un xml transformado para MS-Excel) desde ajax. La función Ajax Success tiene un parámetro de datos que contiene el xml transformado. ¿Cómo usar este parámetro de datos para iniciar una ventana del navegador y mostrar un cuadro de diálogo Guardar como o simplemente abrir Excel?
heredero
@sheir, si desea que el navegador inicie el archivo, no debe cargarlo a través de AJAX. Simplemente navegue directamente a su método de acción. El tipo MIME determinará cómo es manejado por el navegador. Usando algo como application/octet-streamforzarlo a descargar. No sé qué tipo MIME lanza Excel, pero debería poder encontrarlo en línea con la suficiente facilidad.
Drew Noakes
26

Si solo está interesado en devolver xml a través de una solicitud, y tiene su "fragmento" xml, simplemente puede hacer (como una acción en su controlador):

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

Recientemente tuve que hacer esto para un proyecto de Sitecore que utiliza un método para crear un documento Xml a partir de un elemento de Sitecore y sus elementos secundarios y lo devuelve desde el controlador ActionResult como un archivo. Mi solución:

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

Finalmente logré conseguir este trabajo y pensé que documentaría cómo hacerlo aquí con la esperanza de salvar a otros del dolor.

Ambiente

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

Navegadores web compatibles

  • FireFox 23
  • IE 10
  • Chrome 29
  • Opera 16
  • Safari 5.1.7 (¿el último para Windows?)

Mi tarea fue hacer clic en el botón ui, llamar a un método en mi controlador (con algunos parámetros) y luego hacer que devuelva un XML de MS-Excel a través de una transformación xslt. El XML de MS-Excel devuelto provocará que el navegador muestre el cuadro de diálogo Abrir / Guardar. Esto tenía que funcionar en todos los navegadores (enumerados anteriormente).

Al principio intenté con Ajax y crear un Anchor dinámico con el atributo "descargar" para el nombre de archivo, pero eso solo funcionó para aproximadamente 3 de los 5 navegadores (FF, Chrome, Opera) y no para IE o Safari. Y hubo problemas al intentar disparar mediante programación el evento Click del ancla para provocar la "descarga" real.

¡Lo que terminé haciendo fue usar un IFRAME "invisible" y funcionó para los 5 navegadores!

Así que esto es lo que se me ocurrió: [tenga en cuenta que de ninguna manera soy un gurú html / javascript y solo he incluido el código relevante]

HTML (fragmento de bits relevantes)

<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 (fragmento de código) @Drew creó un ActionResult personalizado llamado XmlActionResult que modifiqué para mi propósito.

¿Devolver XML de la acción de un controlador como un ActionResult?

Método My Controller (devuelve ActionResult)

  • pasa el parámetro de claves a un proceso almacenado de SQL Server que genera un XML
  • ese XML luego se transforma a través de xslt en un MS-Excel xml (XmlDocument)
  • crea una instancia del XmlActionResult modificado y lo devuelve

    Resultado XmlActionResult = nuevo XmlActionResult (excelXML, "application / vnd.ms-excel"); versión de cadena = DateTime.Now.ToString ("dd_MMM_yyyy_hhmmsstt"); string fileMask = "LabelExport_ {0} .xml";
    result.DownloadFilename = string.Format (fileMask, versión); resultado de retorno;

La modificación principal a la clase XmlActionResult que @Drew creó.

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);
}

Eso fue básicamente todo. Espero que ayude a los demás.

el heredero
fuente
1

Una opción simple que le permitirá usar transmisiones y todo lo que es return File(stream, "text/xml");.

Casey
fuente
0

Aquí hay una manera simple de hacerlo:

        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");
usuario2670714
fuente
¿Por qué esto genera dos flujos de memoria? ¿Por qué no simplemente pasar msdirectamente, en lugar de copiarlo a uno nuevo? Ambos objetos tendrán la misma vida útil.
jpaugh
Haga un ms.Position=0y puede devolver el flujo de memoria original. Entonces puedesreturn new FileStreamResult(ms,"text/xml");
Carter Medlin
0

Una pequeña variación de la respuesta de Drew Noakes que usa el método 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
fuente