Retornar XML da ação de um controlador como ActionResult?

139

Qual é a melhor maneira de retornar XML da ação de um controlador no ASP.NET MVC? Existe uma boa maneira de retornar JSON, mas não para XML. Eu realmente preciso rotear o XML por meio de um modo de exibição ou devo seguir a maneira não recomendada de Response.Write-ing?

Ken Randall
fonte

Respostas:

114

Use a ação XmlResult do MVCContrib .

Para referência, aqui está o código deles:

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
fonte
12
A turma aqui é tirada diretamente do projeto MVC Contrib. Não tenho certeza se isso é o que qualifica como rolando o seu próprio.
Sailing Judo
3
Onde você colocaria essa classe, se estiver seguindo a convenção do ASP.NET MVC? Pasta Controladores? Mesmo lugar que você colocaria seus ViewModels, talvez?
p.campbell 02/09/09
7
@pcampbel, eu prefiro criar pastas separadas em minha raiz do projeto para todos os tipos de classes: Resultados, Filtros, Routing, etc.
Anthony Serdyukov
As XmlSerialiseranotações de membro e uso podem ser difíceis de manter. Desde que Luke postou essa resposta (cerca de quatro anos atrás), o Linq to XML se provou um substituto mais elegante e poderoso para os cenários mais comuns. Confira minha resposta para um exemplo de como fazer isso.
de Drew Noakes
133
return this.Content(xmlString, "text/xml");
Petr
fonte
1
Uau, isso realmente me ajudou, mas então estou começando a mexer com o MVC.
Denis Valeev 27/05
Se você estiver trabalhando com o Linq to XML, criar um formulário de cadeia de caracteres do documento é um desperdício - é melhor trabalhar com fluxos .
de Drew Noakes
2
@ Drew Noakes: Não, não é. Se você gravar diretamente no fluxo HttpContext.Response.Output, receberá um YSOD nos servidores baseados em WinXP. Parece estar corrigido no Vista +, o que é especialmente problemático se você desenvolver no Windows 7 e implantar no Windows XP (Server 2003?). Se fizer isso, você precisa escrever a um fluxo de memória primeiro, e depois copiar o fluxo de memória para o fluxo de saída ...
Stefan Steiger
6
@ Quandary, ok Vou reafirmar o ponto: criar strings é um desperdício quando você pode evitar alocações / coleções / exceções de falta de memória usando fluxos, a menos que esteja trabalhando em sistemas de computação de 11 anos que exibem um erro.
de Drew Noakes
1
Você pode querer usar o application/xmltipo MIME.
Fred
32

Se você estiver construindo o XML usando a excelente estrutura Linq para XML, essa abordagem será útil.

Eu crio um XDocumentno método de ação.

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

Esse costume reutilizável ActionResultserializa o XML para você.

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

Você pode especificar um tipo MIME (como application/rss+xml) e se a saída deve ser recuada, se necessário. Ambas as propriedades possuem padrões sensíveis.

Se você precisar de uma codificação diferente de UTF8, é simples adicionar uma propriedade para isso também.

Drew Noakes
fonte
Você acha que é possível modificar isso para uso em um controlador de API?
precisa
@ Rayayckley, eu não sei, porque ainda não testei o novo material da API da Web. Se você descobrir, informe-nos.
de Drew Noakes
Eu acho que estava no caminho errado com a questão do controlador de API (normalmente não faço coisas de MVC). Eu apenas o implementei como um controlador regular e funcionou muito bem.
precisa
Bom trabalho, Drew. Estou usando um sabor do seu XmlActionResult para minha exigência. Meu ambiente de desenvolvimento: ASP.NET 4 MVC Chamo o método do meu controlador (retorna XmlActionResult - contendo um xml transformado para MS-Excel) do ajax. A função Ajax Success possui um parâmetro de dados que contém o xml transformado. Como usar esse parâmetro de dados para iniciar uma janela do navegador e exibir uma caixa de diálogo SaveAs ou apenas abrir o Excel?
Sheir
@ shee, se você deseja que o navegador inicie o arquivo, não deve carregá-lo via AJAX. Basta navegar diretamente para o seu método de ação. O tipo MIME determinará como é tratado pelo navegador. Usando algo como application/octet-streamforçar o download. Não sei qual tipo MIME inicia o Excel, mas você deve encontrá-lo online com bastante facilidade.
Desenhou Noakes
26

Se você estiver interessado apenas em retornar o xml através de uma solicitação e tiver seu "chunk" em xml, basta fazer o seguinte (como uma ação no seu controlador):

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

Eu tive que fazer isso recentemente para um projeto Sitecore que usa um método para criar um XmlDocument a partir de um Item Sitecore e seus filhos e o retorna do controlador ActionResult como um arquivo. Minha solução:

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

Finalmente consegui esse trabalho e pensei em documentar como aqui, na esperança de salvar a dor dos outros.

Meio Ambiente

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

Navegadores da Web suportados

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

Minha tarefa foi clicar no botão da interface do usuário, chamar um método no meu Controller (com alguns parâmetros) e depois retornar um XML do MS-Excel por meio de uma transformação xslt. O XML retornado do MS-Excel faria o navegador abrir a caixa de diálogo Abrir / Salvar. Isso teve que funcionar em todos os navegadores (listados acima).

No começo, tentei com o Ajax e criar uma âncora dinâmica com o atributo "download" para o nome do arquivo, mas isso só funcionou para cerca de 3 dos 5 navegadores (FF, Chrome, Opera) e não para o IE ou o Safari. E houve problemas ao tentar disparar programaticamente o evento Click da âncora para causar o "download" real.

O que acabei fazendo foi usar um IFRAME "invisível" e funcionou para todos os 5 navegadores!

Então, aqui está o que eu criei: [por favor, note que eu não sou um guru de html / javascript e incluí apenas o código relevante]

HTML (trecho 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 (trecho de código) @Drew criou um ActionResult personalizado chamado XmlActionResult que eu modifiquei para o meu propósito.

Retornar XML da ação de um controlador como ActionResult?

Meu método Controller (retorna ActionResult)

  • passa o parâmetro keys para um processo armazenado do SQL Server que gera um XML
  • esse XML é então transformado via xslt em um xml do MS-Excel (XmlDocument)
  • cria uma instância do XmlActionResult modificado e o retorna

    Resultado XmlActionResult = novo XmlActionResult (excelXML, "application / vnd.ms-excel"); versão da string = DateTime.Now.ToString ("dd_MMM_yyyy_hhmmsstt"); string fileMask = "LabelExport_ {0} .xml";
    result.DownloadFilename = string.Format (fileMask, versão); resultado de retorno;

A principal modificação na classe XmlActionResult que @Drew criou.

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

Era basicamente isso. Espero que ajude os outros.

herdeiro
fonte
1

Uma opção simples que permitirá que você use fluxos e tudo o que é return File(stream, "text/xml");.

Casey
fonte
0

Aqui está uma maneira simples de fazer isso:

        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
fonte
Por que isso cria dois fluxos de memória? Por que não passar msdiretamente, em vez de copiá-lo para um novo? Ambos os objetos terão a mesma vida útil.
jpaugh
Faça um ms.Position=0e você pode retornar o fluxo de memória original. Então você podereturn new FileStreamResult(ms,"text/xml");
Carter Medlin
0

Uma pequena variação da resposta de Drew Noakes que usa o método Save () do 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
fonte