2

I need a way to pass a list of strings when a user clicks an icon from angular/script and send it to an MVC controller in .NET. This list does what it needs to then is supposed to download a file in my browser. I have learned that I cannot do this via AJAX and/or it get pretty messy.

Edit: the list of strings refers to a list of file ids that I am retrieving, then zipping up into one file, which is to be downloaded. I do not want to store this zipped file anywhere permanently.

I am open to ideas!

        $http.post('document/downloadfiles', data).success(function ()         {/*success callback*/ });

    [HttpPost]
    public ActionResult DownloadFiles(List<string> fileUniqueIdentifiers)
    {
           var file =    _service.ArchiveAndDownloadDocuments(fileUniqueIdentifiers);

        file.Position = 0;

        return new FileStreamResult(file, "application/force-download");


    }

3 Answers3

1

I normally wouldn't go to this length of effort to help, but I'm home sick and feel like writing some code, so here's an implementation of what I think you're asking for. Here I'm using token exchange to track the file interchange for a specific user storing the data in a singleton instance, you could use another method (e.g. database token storage) if you wanted...

View Part (I added mine to index.cshtml):

<script type="text/javascript">
    function sendStringandGetFiles() {

        var strings = ['One String', 'Two String', 'Three String'];

        $.ajax({
            type: "POST",
            url: "/Home/GetFile",
            contentType: 'application/json',
            data: JSON.stringify(strings),
            success: function (result) {
                //alert("Yes This worked! - " + result);
                window.location = "/Home/GetFile?token=" + result;
            }
        });
    }
</script>

<h5>Just something to click</h5>
<button onclick="sendStringandGetFiles()">Send String and Get Files</button>

Then, Controller part (I used HomeController.cs):

[AcceptVerbs(HttpVerbs.Post)]
public string GetFile(string[] strings)
{
    for (int i = 0; i < strings.Length; i++)
    {
        // Do some stuff with string array here.
    }
    Guid token = Guid.NewGuid();
    InMemoryInstances instance = InMemoryInstances.Instance;
    instance.addToken(token.ToString());

    return token.ToString();
}

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult GetFile(string token)
{
    string filename = @"c:\temp\afile.txt";

    InMemoryInstances instance = InMemoryInstances.Instance;

    if (instance.checkToken(token))
    {
        instance.removeToken(token);
        FileStreamResult resultStream = new FileStreamResult(new FileStream(filename, FileMode.Open, FileAccess.Read), "txt/plain");

        resultStream.FileDownloadName = Path.GetFileName(filename);

        return resultStream;
    }
    else
    {
        return View("Index");
    }

}

InMemoryInstances Class:

public class InMemoryInstances
{
    private static volatile InMemoryInstances instance;
    private static object syncRoot = new Object();

    private List<Guid> activeTokens;

    private InMemoryInstances() 
    {
        activeTokens = new List<Guid>();
    }

    public static InMemoryInstances Instance
    {
        get 
        {
            if (instance == null) 
            {
                lock (syncRoot)                  
                {
                    if (instance == null) 
                        instance = new InMemoryInstances();
                }
            }

            return instance;
        }
    }

    public bool checkToken(string token)
    {
        return activeTokens.Contains(new Guid(token));
    }

    public bool addToken(string token)
    {
        activeTokens.Add(new Guid(token));
        return true;
    }

    public bool removeToken(string token)
    {
        return activeTokens.Remove(new Guid(token));
    }
}

Hope this helps!

OBR_EXO
  • 600
  • 4
  • 19
1

I also wrote yet another implementation which uses cookies to perform the same operation (incase you wanted to store the information client-side instead of using a query-string, yes, I'm slightly bored)...

View Part (I added mine to index.cshtml):

<script type="text/javascript">
    function sendStringandGetFiles() {

        var strings = ['One String', 'Two String', 'Three String'];

        $.ajax({
            type: "POST",
            url: "/Home/GetFile",
            contentType: 'application/json',
            data: JSON.stringify(strings),
            success: function (result) {
                //alert("Yes This worked! - " + result);
                window.location = "/Home/GetFile?token=" + result;
            }
        });
    }
</script>

<h5>Just something to click</h5>
<button onclick="sendStringandGetFiles()">Send String and Get Files</button>

Then Controller Part (I used HomeController.cs)

[AcceptVerbs(HttpVerbs.Post)]
public string GetFile(string[] strings)
{
    for (int i = 0; i < strings.Length; i++)
    {
        // Do some stuff with string array here.
    }
    Guid token = Guid.NewGuid();
    InMemoryInstances instance = InMemoryInstances.Instance;

    instance.addToken(token.ToString());

    HttpCookie cookie = new HttpCookie("CookieToken");

    cookie.Value = token.ToString();

    this.ControllerContext.HttpContext.Response.Cookies.Add(cookie);

    return token.ToString();
}

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult GetFile()
{
    string filename = @"c:\temp\afile.txt";

    InMemoryInstances instance = InMemoryInstances.Instance;

    if (this.ControllerContext.HttpContext.Request.Cookies.AllKeys.Contains("CookieToken"))
    {
        HttpCookie cookie = this.ControllerContext.HttpContext.Request.Cookies["CookieToken"];

        if (instance.checkToken(cookie.Value)) 
        {                                
            cookie.Expires = DateTime.Now.AddDays(-1);            
              this.ControllerContext.HttpContext.Response.Cookies.Add(cookie);

            FileStreamResult resultStream = new FileStreamResult(new FileStream(filename, FileMode.Open, FileAccess.Read), "txt/plain");

            resultStream.FileDownloadName = Path.GetFileName(filename);

            return resultStream;
        } else 
        {
            return View("Index");
        }
    }            
    else
    {
        return View("Index");
    }

}

InMemoryInstances Class:

public class InMemoryInstances
{
    private static volatile InMemoryInstances instance;
    private static object syncRoot = new Object();

    private List<Guid> activeTokens;

    private InMemoryInstances() 
    {
        activeTokens = new List<Guid>();
    }

    public static InMemoryInstances Instance
    {
        get 
        {
            if (instance == null) 
            {
                lock (syncRoot)                  
                {
                    if (instance == null) 
                        instance = new InMemoryInstances();
                }
            }

            return instance;
        }
    }

    public bool checkToken(string token)
    {
        return activeTokens.Contains(new Guid(token));
    }

    public bool addToken(string token)
    {
        activeTokens.Add(new Guid(token));
        return true;
    }

    public bool removeToken(string token)
    {
        return activeTokens.Remove(new Guid(token));
    }
}

Maybe that is better if you want to hide the token interchange from the browser address bar?

OBR_EXO
  • 600
  • 4
  • 19
  • Why didn't you use a `JsonResult`, it is designed for exactly that. The framework will deserialize your JSON for you, assuming the parameters match exactly. – Greg Mar 17 '15 at 13:54
  • 1
    Thank you so much for being bored! I really appreciate. I'm trying to understand something though, and I probably did not explain my situation good enough. Assumption: This implementation assumes the file is stored somewhere on the server? The numerous strings I am passing in to the original controller method are just unique file ids that I go and get, and then archive mulitple files into a zip...So I assume I need to store the zip somewhere temporarily? – jimfromthegym - Jim Mackin Mar 17 '15 at 13:58
  • 1
    So now that the coffee has set in, I implemented and see how this works. But for what I'm trying to do, ( create a zipped file of multiple files ), does it make sense that I would need to save the file somewhere then go get it, download it, and delete it? I definitely don't want it saved anywhere permanently. What are some best practices? – jimfromthegym - Jim Mackin Mar 17 '15 at 14:22
  • I've written you one more answer Jim - Should cover what you asked, I think at this point you've got more than enough to work with... – OBR_EXO Mar 18 '15 at 03:41
1

Ok, third-time lucky I guess? (I think this'll be my last implementation sorry Jim - you'll have to work the rest out for yourself, I think I've given you more than enough free pointers for now... if you want more you can contact me and I'll charge you to write it! :P).

This version uses a cookie-based interchange, accepts the input strings (assuming they are filenames) from the javascript, stores these in the instance class along with the token as key, then assembles the ZipFile in-memory (without writing to disk), and then returns the zipfile as a Content Result. For efficiency you could remove the actual token checks against the GUID list and just check against the key in the file list. Obviously you probably won't want the filenames hard-coded in that javascript as I've done, but you can work that part out for yourself. Hint: Create a database table with identifier/filepath pairs and use the identifiers to lookup the individual file paths after the request is sent to the server...

View Part (I added mine to index.cshtml):

<script type="text/javascript">
    function sendStringandGetFiles() {

        var files = ['c:\\temp\\afile.txt', 'c:\\temp\\afile2.txt', 'c:\\temp\\afile3.txt'];

        $.ajax({
            type: "POST",
            url: "/Home/GetFile",
            contentType: 'application/json',
            data: JSON.stringify(files),
            success: function (result) {
                //alert("Yes This worked! - " + result);
                window.location = "/Home/GetFile";
            }
        });
    }
</script>

<h5>Just something to click</h5>
<button onclick="sendStringandGetFiles()">Send String and Get Files</button>

Then Controller Part (I used HomeController.cs)

    [AcceptVerbs(HttpVerbs.Post)]
    public string GetFile(string[] strings)
    {
        Guid token = Guid.NewGuid();

        InMemoryInstances instance = InMemoryInstances.Instance;

        instance.addToken(token.ToString());

        instance.addFiles(token.ToString(), strings);

        HttpCookie cookie = new HttpCookie("CookieToken");

        cookie.Value = token.ToString();

        this.ControllerContext.HttpContext.Response.Cookies.Add(cookie);

        return token.ToString();
    }

    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult GetFile()
    {
        InMemoryInstances instance = InMemoryInstances.Instance;

        if (this.ControllerContext.HttpContext.Request.Cookies.AllKeys.Contains("CookieToken"))
        {
            HttpCookie cookie = this.ControllerContext.HttpContext.Request.Cookies["CookieToken"];

            if (instance.checkToken(cookie.Value)) 
            {                                
                cookie.Expires = DateTime.Now.AddDays(-1);

                this.ControllerContext.HttpContext.Response.Cookies.Add(cookie);

                MemoryStream ms = new MemoryStream();

                string[] filenames = instance.getFiles(cookie.Value);

                using (ZipArchive zs = new ZipArchive(ms,ZipArchiveMode.Create, true))
                {
                    for (int i=0;i < filenames.Length; i++)
                        zs.CreateEntryFromFile(filenames[i], Path.GetFileName(filenames[i]));                        
                }

                FileContentResult resultContent = new FileContentResult(ms.ToArray(),"application/zip");

                instance.removeFiles(cookie.Value);

                resultContent.FileDownloadName = "ARandomlyGeneratedFileNameHere.zip";

                return resultContent;
            } else 
            {
                return View("Index");
            }
        }            
        else
        {
            return View("Index");
        }

    }

InMemoryInstances Class:

public class InMemoryInstances
{
    private static volatile InMemoryInstances instance;
    private static object syncRoot = new Object();

    private List<Guid> activeTokens;
    private NameValueCollection filesByKeyCollection;

    private InMemoryInstances() 
    {
        activeTokens = new List<Guid>();
        filesByKeyCollection = new NameValueCollection();
    }

    public static InMemoryInstances Instance
    {
        get 
        {
            if (instance == null) 
            {
                lock (syncRoot)                  
                {
                    if (instance == null) 
                        instance = new InMemoryInstances();
                }
            }

            return instance;
        }
    }

    public bool checkToken(string token)
    {
        return activeTokens.Contains(new Guid(token));
    }

    public string[] getFiles(string token)
    {
        return filesByKeyCollection.GetValues(token);
    }

    public bool addFiles(string token, string[] files)
    {
        for (int i = 0; i < files.Length; i++)
            filesByKeyCollection.Add(token, files[i]);

        return true;
    }

    public bool addToken(string token)
    {
        activeTokens.Add(new Guid(token));
        return true;
    }

    public bool removeFiles(string token)
    {
        filesByKeyCollection.Remove(token);
        return true;
    }

    public bool removeToken(string token)
    {
        return activeTokens.Remove(new Guid(token));
    }
}

Hope this helps!

OBR_EXO
  • 600
  • 4
  • 19
  • I greatly appreciate everyone's efforts. I had implemented something similar and this does answer my question. I guess I would like to know what other sites are doing too? Is this best practice compared to what everyone else is doing? I saw another answer in a different forum where someone was storing them in a different location temporarily which seemed to be a bit extreme. – jimfromthegym - Jim Mackin Mar 18 '15 at 13:28