Zwrócić XML z akcji kontrolera w postaci ActionResult?

139

Jaki jest najlepszy sposób na zwrócenie kodu XML z akcji kontrolera w ASP.NET MVC? Jest dobry sposób na zwrócenie JSON, ale nie w przypadku XML. Czy naprawdę muszę kierować XML przez widok, czy powinienem zastosować niezbyt sprawdzony sposób odpowiedzi.

Ken Randall
źródło

Odpowiedzi:

114

Użyj akcji XmlResult MVCContrib.

W celach informacyjnych tutaj jest ich kod:

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
źródło
12
Klasa tutaj jest pobierana bezpośrednio z projektu MVC Contrib. Nie jestem pewien, czy to kwalifikuje się jako skręcanie własnego.
Sailing Judo
3
Gdzie umieściłbyś tę klasę, jeśli postępujesz zgodnie z konwencją ASP.NET MVC? Folder kontrolerów? Być może w tym samym miejscu, w którym umieścisz swoje ViewModels?
p.campbell
7
@pcampbel, wolę tworzyć osobne foldery w katalogu głównym mojego projektu dla każdego rodzaju zajęć: wyniki, filtry, routing itp.
Anthony Serdyukov
Korzystanie z XmlSerialiseradnotacji i członków może być trudne do utrzymania. Odkąd Luke opublikował tę odpowiedź (około cztery lata temu), Linq to XML okazał się bardziej eleganckim i wydajnym zamiennikiem większości typowych scenariuszy. Sprawdź moją odpowiedź, aby zobaczyć przykład, jak to zrobić.
Drew Noakes
133
return this.Content(xmlString, "text/xml");
Petr
źródło
1
Wow, to naprawdę mi pomogło, ale potem dopiero zaczynam majstrować przy tej sprawie z MVC.
Denis Valeev
Jeśli pracujesz z Linq do XML, tworzenie postaci ciągu dokumentu jest marnotrawstwem - lepiej pracować ze strumieniami .
Drew Noakes
2
@Drew Noakes: Nie, nie jest. Jeśli napiszesz bezpośrednio do strumienia HttpContext.Response.Output, otrzymasz YSOD na serwerach opartych na WinXP. Wydaje się, że problem został rozwiązany w systemie Vista +, co jest szczególnie problematyczne, jeśli tworzysz w systemie Windows 7 i wdrażasz w systemie Windows XP (Server 2003?). Jeśli tak, musisz najpierw zapisać do strumienia pamięci, a następnie skopiować strumień pamięci do strumienia wyjściowego ...
Stefan Steiger
6
@Quandary, ok, powtórzę sprawę: tworzenie łańcuchów jest marnotrawstwem, gdy można uniknąć alokacji / zbierania / wyjątków braku pamięci za pomocą strumieni, chyba że pracujesz na 11-letnich systemach komputerowych, które wykazują błąd.
Drew Noakes
1
Zamiast tego możesz użyć application/xmltypu MIME.
Fred
32

Jeśli budujesz XML przy użyciu doskonałego frameworka Linq-to-XML, to podejście będzie pomocne.

Tworzę metodę XDocumentw akcji.

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

Ten niestandardowy, wielokrotnego użytku, ActionResultserializuje XML dla Ciebie.

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

Możesz określić typ MIME (np application/rss+xml ) i określić, czy dane wyjściowe powinny być wcięte, jeśli zajdzie taka potrzeba. Obie właściwości mają rozsądne wartości domyślne.

Jeśli potrzebujesz kodowania innego niż UTF8, możesz łatwo dodać właściwość również do tego.

Drew Noakes
źródło
Czy uważasz, że można to zmodyfikować do użytku w kontrolerze API?
Ray Ackley
@RayAckley, nie wiem, ponieważ nie wypróbowałem jeszcze nowego interfejsu API sieci Web. Jeśli się dowiesz, daj nam znać.
Drew Noakes
Myślę, że byłem na złym tropie z pytaniem o kontroler API (normalnie nie robię rzeczy MVC). Po prostu zaimplementowałem to jako zwykły kontroler i działało świetnie.
Ray Ackley
Świetna robota Drew. Używam smaku Twojego XmlActionResult dla moich wymagań. Moje środowisko deweloperskie: ASP.NET 4 MVC Wywołuję metodę mojego kontrolera (zwraca XmlActionResult - zawierający przekształcony plik XML dla MS-Excel) z ajax. Funkcja Sukces Ajax ma parametr danych, który zawiera przekształcony plik XML. Jak użyć tego parametru danych, aby uruchomić okno przeglądarki i wyświetlić okno dialogowe Zapisz jako lub po prostu otworzyć program Excel?
sheir
@sheir, jeśli chcesz, aby przeglądarka uruchomiła plik, nie powinieneś go ładować przez AJAX. Po prostu przejdź bezpośrednio do swojej metody działania. Typ MIME określa, jak jest obsługiwany przez przeglądarkę. Używanie czegoś w rodzaju application/octet-streamwymuszenia pobrania. Nie wiem, jaki typ MIME uruchamia program Excel, ale powinno być łatwo znaleźć go w Internecie.
Drew Noakes,
26

Jeśli chcesz zwrócić plik XML tylko za pośrednictwem żądania, a masz swój „fragment” xml, możesz po prostu wykonać (jako akcję w kontrolerze):

public string Xml()
{
    Response.ContentType = "text/xml";
    return yourXmlChunk;
}
Erik
źródło
4

Musiałem to zrobić ostatnio dla projektu Sitecore, który używa metody do tworzenia XmlDocument z elementu Sitecore i jego elementów podrzędnych i zwraca go z kontrolera ActionResult jako plik. Moje rozwiązanie:

public virtual ActionResult ReturnXml()
{
    return File(Encoding.UTF8.GetBytes(GenerateXmlFeed().OuterXml), "text/xml");
}
Matthew Price
źródło
2

W końcu udało mi się zdobyć tę pracę i pomyślałem, że udokumentuję, jak tutaj w nadziei uratowania bólu innym.

Środowisko

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

Obsługiwane przeglądarki internetowe

  • FireFox 23
  • IE 10
  • Chrome 29
  • Opera 16
  • Safari 5.1.7 (ostatnia dla Windows?)

Moje zadanie polegało na kliknięciu przycisku interfejsu użytkownika, wywołaniu metody na moim kontrolerze (z kilkoma parametrami), a następnie zwróceniu pliku XML MS-Excel za pośrednictwem transformacji xslt. Zwrócony plik XML MS-Excel spowodowałby wówczas wyświetlenie okna dialogowego Otwórz / Zapisz w przeglądarce. Musiało to działać we wszystkich przeglądarkach (wymienionych powyżej).

Na początku próbowałem z Ajaxem i stworzyć dynamiczną kotwicę z atrybutem „download” dla nazwy pliku, ale działało to tylko dla około 3 z 5 przeglądarek (FF, Chrome, Opera), a nie dla IE czy Safari. Występowały też problemy z próbą programowego wywołania zdarzenia Click kotwicy w celu spowodowania faktycznego „pobrania”.

Skończyło się na tym, że użyłem „niewidzialnej” ramki IFRAME i zadziałało we wszystkich 5 przeglądarkach!

Oto, co wymyśliłem: [pamiętaj, że w żadnym wypadku nie jestem guru html / javascript i włączyłem tylko odpowiedni kod]

HTML (fragment odpowiednich bitów)

<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 (fragment kodu) @Drew utworzył niestandardową ActionResult o nazwie XmlActionResult, którą zmodyfikowałem w swoim celu.

Zwrócić XML z akcji kontrolera w postaci ActionResult?

Metoda My Controller (zwraca ActionResult)

  • przekazuje parametr keys do przechowywanego procesu SQL Server, który generuje XML
  • ten XML jest następnie przekształcany przez xslt w plik XML MS-Excel (XmlDocument)
  • tworzy wystąpienie zmodyfikowanego XmlActionResult i zwraca je

    XmlActionResult result = new XmlActionResult (excelXML, "application / vnd.ms-excel"); string version = DateTime.Now.ToString ("dd_MMM_yyyy_hhmmsstt"); string fileMask = "LabelExport_ {0} .xml";
    result.DownloadFilename = string.Format (fileMask, wersja); wynik zwrotu;

Główna modyfikacja klasy XmlActionResult utworzonej przez @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);
}

To było w zasadzie to. Mam nadzieję, że to pomaga innym.

Sheir
źródło
1

Prosta opcja, która pozwoli Ci korzystać ze strumieni i tego wszystkiego return File(stream, "text/xml");.

Casey
źródło
0

Oto prosty sposób na zrobienie tego:

        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
źródło
Dlaczego to tworzy dwa strumienie pamięci? Dlaczego po prostu nie przejść msbezpośrednio, zamiast kopiować go do nowego? Oba obiekty będą miały taką samą żywotność.
jpaugh
Zrób a ms.Position=0i możesz zwrócić oryginalny strumień wspomnień. Wtedy możeszreturn new FileStreamResult(ms,"text/xml");
Carter Medlin
0

Mała odmiana odpowiedzi od Drew Noakes, która używa metody Save () z 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
źródło