AssetManager vs Internal Storage: How can I store cross-platform config.json file in Xamarin Android that I want to edit later? - xamarin.android

I want to be able to pre-configure my Android app to include a json file that is read/writeable, that is usable on other platforms as well (iOS, UWP). I started looking at AssetManager but found that I could only read the file, not edit it later.
Is internal storage the answer? If so, how do I pre-populate the app's internal storage with this file? I couldn't find any documentation online on how to do this besides performing a full write method in C# code, which kind of defeats the purpose of sharing the same config.json file on all platforms. I just want my Android app to store this config.json file, and have the file be readable and writeable.
Any help would be greatly appreciated.

I decided to answer my own question and show how it can be done in code. In this answer, I am using both an Android asset and internal storage to manipulate the file that contains my serialized object.
1) Copy your file into the Xamarin.Android project's Assets folder, and change its Build Action to "AndroidAsset". This will ensure that the file is packaged with your Android app initially.
2) In your C# code, what you really want is to read and write this file from your Android App's internal storage, not from the AssetManager (you can't write to the AssetManager anyways, only read!). So to do this, you must first pull the file from the AssetManager and then all subsequent read/writes will occur from internal storage.
/// <summary>
/// This method will read the application settings from internal storage.
/// If it can't find it, it will use the default AndroidAsset version of
/// the appsettings.json file from this project's Asset folder.
/// </summary>
/// <returns></returns>
public async Task<DefaultSettings> GetDefaultSettingsAsync()
{
var path = Application.Context.FilesDir.Path;
if (File.Exists(Path.Combine(path, "appsettings.json")))
{
using (var sr = Application.Context.OpenFileInput("appsettings.json"))
{
using (var ms = new MemoryStream())
{
await sr.CopyToAsync(ms);
var memoryBytes = ms.ToArray();
var content = System.Text.Encoding.Default.GetString(memoryBytes);
var defaultSettings = JsonConvert.DeserializeObject<DefaultSettings>(content);
return defaultSettings;
}
}
}
else
{
var result = await GetDefaultSettingsAssetAsync();
return result;
}
}
/// <summary>
/// This method gets the appsettings.json file from the Android AssetManager, in the case
/// where you do not have an editable appsettings.json file yet in your Android app's internal storage.
/// </summary>
/// <returns></returns>
private static async Task<DefaultSettings> GetDefaultSettingsAssetAsync()
{
try
{
var assetsManager = MainActivity.Instance.Assets;
using (var sr = assetsManager.Open("appsettings.json"))
{
using (var ms = new MemoryStream())
{
await sr.CopyToAsync(ms);
var memoryBytes = ms.ToArray();
var content = System.Text.Encoding.Default.GetString(memoryBytes);
var defaultSettings = JsonConvert.DeserializeObject<DefaultSettings>(content);
// This will write the Asset to Internal Storage for the first time. All subsequent writes will occur using SaveDefaultSettingsAsync.
using (var stream = Application.Context.OpenFileOutput("appsettings.json", FileCreationMode.Private))
{
await stream.WriteAsync(memoryBytes, 0, memoryBytes.Length);
}
return defaultSettings;
}
}
}
catch (Exception ex)
{
return new DefaultSettings();
}
}
/// <summary>
/// This method will save the DefaultSettings to your appsettings.json file in your Android App's internal storage.
/// </summary>
/// <param name="settings"></param>
/// <returns></returns>
public async Task SaveDefaultSettingsAsync(DefaultSettings settings)
{
using (var stream = Application.Context.OpenFileOutput("appsettings.json", FileCreationMode.Private))
{
var content = JsonConvert.SerializeObject(settings);
var bytes = Encoding.Default.GetBytes(content);
await stream.WriteAsync(bytes, 0, bytes.Length);
}
}

Related

How to get Sqlite Connection in Xamarin Forms in iOS?

I've created a Xamarin.Forms PCL project and trying to access the local data stored in sqlite database which is working file in Android but not working in iOS. Whenever I'm trying to call the iOS specific code using DependencyService it throws System.NullReferenceException: Object reference not set to an instance of an object.
Here is my calling statement
var db = DependencyService.Get<IDBPath>().GetDBPath();
Here is my iOS specific code for getting Sqlite Connection
using SQLite.Net;
using SQLite.Net.Async;
using SQLite.Net.Platform.XamarinIOS;
using SwachhParyatanApp.iOS;
using System;
using System.IO;
[assembly: Xamarin.Forms.Dependency(typeof(DBPath_iOS))]
namespace SwachhParyatanApp.iOS
{
class DBPath_iOS
{
public SQLiteAsyncConnection GetDBPath()
{
var sqliteFilename = "localData.db";
string folder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
string libraryPath = Path.Combine(folder, "..", "Library");
var path = Path.Combine(libraryPath, sqliteFilename);
var platform = new SQLitePlatformIOS();
var param = new SQLiteConnectionString(path, false);
var connection = new SQLiteAsyncConnection(() => new SQLiteConnectionWithLock(platform, param));
return connection;
}
}
}
I don't think the calling method is going to reach the iOS specific code because I used the break point in iOS specific code but it never came to the break point and it immediately gives the error. I've also tried going to the exception for details but there is no inner exception and in stacktrace it only points to the line which called the method.
Using SQLite.Net PCL below is a working example of an iOS dependency injection recipient for SQLite. A couple of differences I noticed are your db extension .db instead of .db3 and your 'assembly' header does not implement the full namespace. I am not sure if that matters.
[assembly: Dependency(typeof(NameSpace.iOS.SQLiteUtility.SQLite_iOS))]
namespace NameSpace.iOS.SQLiteUtility
{
class SQLite_iOS : ISQLite
{
public SQLiteConnection GetConnection()
{
try
{
var sqliteFilename = "MyDB.db3";
string documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal); // Documents folder
string libraryPath = Path.Combine(documentsPath, "..", "Library"); // Library folder
var path = Path.Combine(libraryPath, sqliteFilename);
var plat = new SQLite.Net.Platform.XamarinIOS.SQLitePlatformIOS();
var conn = new SQLite.Net.SQLiteConnection(plat, path,
SQLite.Net.Interop.SQLiteOpenFlags.ReadWrite |
SQLite.Net.Interop.SQLiteOpenFlags.Create |
SQLite.Net.Interop.SQLiteOpenFlags.FullMutex, true);
return conn;
}
catch (SQLiteException ex)
{
Helpers.Helper_ErrorHandling.SendErrorToServer(ex);
return null;
}
catch (System.Exception ex)
{
Helpers.Helper_ErrorHandling.SendErrorToServer(ex);
return null;
}
}
}
If it must be the async version you may want to look at How to use SQLiteAsyncConnection from the async PCL version of SQLite?

File Name from HttpRequestMessage Content

I implemented a POST Rest service to upload files to my server. the problem i have right now is that i want to restrict the uploaded files by its type. lets say for example i only want to allow .pdf files to be uploaded.
What I tried to do was
Task<Stream> task = this.Request.Content.ReadAsStreamAsync();
task.Wait();
FileStream requestStream = (FileStream)task.Result;
but unfortunately its not possible to cast the Stream to a FileStream and access the type via requestStream.Name.
is there an easy way (except writing the stream to the disk and check then the type) to get the filetype?
If you upload file to Web API and you want to get access to file data (Content-Disposition) you should upload the file as MIME multipart (multipart/form-data).
Here I showed some examples on how to upload from HTML form, Javascript and from .NET.
You can then do something like this, this example checks for pdf/doc files only:
public async Task<HttpResponseMessage> Post()
{
if (!Request.Content.IsMimeMultipartContent())
{
throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotAcceptable,
"This request is not properly formatted - not multipart."));
}
var provider = new RestrictiveMultipartMemoryStreamProvider();
//READ CONTENTS OF REQUEST TO MEMORY WITHOUT FLUSHING TO DISK
await Request.Content.ReadAsMultipartAsync(provider);
foreach (HttpContent ctnt in provider.Contents)
{
//now read individual part into STREAM
var stream = await ctnt.ReadAsStreamAsync();
if (stream.Length != 0)
{
using (var ms = new MemoryStream())
{
//do something with the file memorystream
}
}
}
return Request.CreateResponse(HttpStatusCode.OK);
}
}
public class RestrictiveMultipartMemoryStreamProvider : MultipartMemoryStreamProvider
{
public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
{
var extensions = new[] {"pdf", "doc"};
var filename = headers.ContentDisposition.FileName.Replace("\"", string.Empty);
if (filename.IndexOf('.') < 0)
return Stream.Null;
var extension = filename.Split('.').Last();
return extensions.Any(i => i.Equals(extension, StringComparison.InvariantCultureIgnoreCase))
? base.GetStream(parent, headers)
: Stream.Null;
}
}

STS Keyset does not exist even after MMC permission granted

My application creates virtual directories on the fly as well as application pool for the STS-enabled web applications that run in those virtual directories. The application pools run under the ApplicationPoolIdentity account (IIS APPPOOL\MyAppPool). And I've been trying to figure out ways of programmatically grant access to the installed certificate.
My first approach was to use a batch file executing WinHttpCertCfg. However this approach will only work on app pool accounts that have been "activated". By "activated", I mean I have browsed to the new application at least once. Until this happens - WinHttpCertCfg always returns the message "The handle is invalid".
The next approach I tried was based on a solution obtained from here. This solution works in the sense that when I browse the certificate in MMC and select "Manage Certificate Keys", the app pool accounts are listed. Even when I run WinHttpCertCfg to list the accounts with access - the new app pools are listed.
But after all that...I still get "keyset does not exist" when I browse the web application.
My focus now is on fixing the second approach. Here is my modofication to the original code
public class CertificateHandler
{
private const string CommonApplicationKeys = #"Microsoft\Crypto\RSA\MachineKeys";
private const string PersonalKeys = #"Microsoft\Crypto\RSA\";
private static X509Certificate2 _personalCertificate = null;
private static X509Certificate2 _trustedCertificate = null;
public CertificateHandler(string thumbPrint)
{
X509Store personalStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);
X509Store trustedStore = new X509Store(StoreName.TrustedPeople, StoreLocation.LocalMachine);
//open the stores to locate the certificates and cache for future use
if (_personalCertificate == null)
{
_personalCertificate = LoadCertificateFromStore(thumbPrint, personalStore);
}
if (_trustedCertificate == null)
{
_trustedCertificate = LoadCertificateFromStore(thumbPrint, trustedStore);
}
}
/// <summary>
/// Grants access to the specified certificate.
/// </summary>
/// <param name="thumbPrint">The thumb print of the certificate.</param>
/// <param name="user">The domain qualified user.</param>
public void GrantAccessToCertificate(string user)
{
//open the store to locate the certificate
GrantAccessToCertificate(user, _personalCertificate);
GrantAccessToCertificate(user, _trustedCertificate);
}
/// <summary>
/// Grants access to the specified certificate.
/// </summary>
/// <param name="user">The domain qualified user.</param>
/// <param name="certificate">The certificate to which access is granted</param>
private void GrantAccessToCertificate(string user, X509Certificate2 certificate)
{
RSACryptoServiceProvider crypto = certificate.PrivateKey as RSACryptoServiceProvider;
if (crypto != null)
{
//determine the location of the key
string keyfilepath = FindKeyLocation(crypto.CspKeyContainerInfo.UniqueKeyContainerName);
//obtain a file handle on the certificate
FileInfo file = new FileInfo(Path.Combine(keyfilepath, crypto.CspKeyContainerInfo.UniqueKeyContainerName));
//Add the user to the access control list for the certificate
FileSecurity fileControl = file.GetAccessControl();
NTAccount account = new NTAccount(user);
fileControl.AddAccessRule(new FileSystemAccessRule(account, FileSystemRights.FullControl, AccessControlType.Allow));
file.SetAccessControl(fileControl);
}
}
/// <summary>
/// Loads the certificate mathing the thumbprint from the specified store.
/// </summary>
/// <param name="thumbPrint">The thumb print of the certificate.</param>
/// <param name="store">The store.</param>
private X509Certificate2 LoadCertificateFromStore(string thumbPrint, X509Store store)
{
X509Certificate2 cert = null;
try
{
//fetch the certificates in the store
store.Open(OpenFlags.MaxAllowed);
//locate by the specified thumbprint
var results = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true);
if (results.Count > 0)
{
cert = results[0];
}
else
{
throw new FileNotFoundException("No certificate was found matching the specified thumbprint");
}
}
finally
{
store.Close();
}
return cert;
}
/// <summary>
/// Finds the key location.
/// </summary>
/// <param name="keyFileName">Name of the key file.</param>
/// <returns></returns>
private string FindKeyLocation(string keyFileName)
{
string location = string.Empty;
//start the search from the common application folder
string root = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
string commonLocation = Path.Combine(root, CommonApplicationKeys);
//filter for the key name
var keys = Directory.GetFiles(commonLocation, keyFileName);
if (keys.Length > 0)
{
location = commonLocation;
}
else
{
//now try the personal application folder
root = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string privateLocation = Path.Combine(root, PersonalKeys);
var subFolders = Directory.GetDirectories(privateLocation);
if (subFolders.Length > 0)
{
foreach (string folder in subFolders)
{
//filter for the key
keys = Directory.GetFiles(folder, keyFileName);
if (keys.Length != 0)
{
location = folder;
}
}
}
else
{
throw new InvalidOperationException("Private key exists but is not accessible");
}
}
return location;
}
}
I can now confirm that the code above actually works. The reason it appeared it wasn't working was that there was another app pool account to which I hadn't granted access to the certificate. Once that was done, it was all rosy from there on.

Localization in MonoDroid

My app is localized using the standard .NET RESX methods (ie. String.fr.resx, Strings.de.resx etc.) works great under Windows Phone.
I am porting to Android using MonoDroid and I do not see the localized UI when I switch locales on the phone. If I rename the APK file to ZIP and open it I see that it has not packaged up the locale DLLs produced during the build (ie. the intermediate \.Resources.dll files are under the bin directory but are not packaged into the APK).
What am I missing? I have tried changing the build action on the RESX files from "Embedded Resource" to "Android Resource" and even "Android Asset" but to no avail.
Thanks in advance for any help!
Cheers
Warren
I asked about this on the monodroid irc channel and the official answer was "not supported yet but we do have plans to do it".
You need to convert the resx files to android xml format (see below) and add them to your project as shown here: http://docs.xamarin.com/android/tutorials/Android_Resources/Part_5_-_Application_Localization_and_String_Resources
In my app (game) I needed to look up the localised strings by name. The code to do this was simple but not immediately obvious. Instead of using ResourceManager I swapped in this for android:
class AndroidResourcesProxy : Arands.Core.IResourcesProxy
{
Context _context;
public AndroidResourcesProxy(Context context)
{
_context = context;
}
public string GetString(string key)
{
int resId = _context.Resources.GetIdentifier(key, "string", _context.PackageName);
return _context.Resources.GetString(resId);
}
}
Since I'm not a XSLT guru I made a command line program for converting resx to Android string XML files:
/// <summary>
/// Conerts localisation resx string files into the android xml format
/// </summary>
class Program
{
static void Main(string[] args)
{
string inFile = args[0];
XmlDocument inDoc = new XmlDocument();
using (XmlTextReader reader = new XmlTextReader(inFile))
{
inDoc.Load(reader);
}
string outFile = Path.Combine(Path.GetDirectoryName(inFile), Path.GetFileNameWithoutExtension(inFile)) + ".xml";
XmlDocument outDoc = new XmlDocument();
outDoc.AppendChild(outDoc.CreateXmlDeclaration("1.0", "utf-8", null));
XmlElement resElem = outDoc.CreateElement("resources");
outDoc.AppendChild(resElem);
XmlNodeList stringNodes = inDoc.SelectNodes("root/data");
foreach (XmlNode n in stringNodes)
{
string key = n.Attributes["name"].Value;
string val = n.SelectSingleNode("value").InnerText;
XmlElement stringElem = outDoc.CreateElement("string");
XmlAttribute nameAttrib = outDoc.CreateAttribute("name");
nameAttrib.Value = key;
stringElem.Attributes.Append(nameAttrib);
stringElem.InnerText = val;
resElem.AppendChild(stringElem);
}
XmlWriterSettings xws = new XmlWriterSettings();
xws.Encoding = Encoding.UTF8;
xws.Indent = true;
xws.NewLineChars = "\n";
using (StreamWriter sr = new StreamWriter(outFile))
{
using (XmlWriter writer = XmlWriter.Create(sr, xws))
{
outDoc.Save(writer);
}
}
}
}

ASP.NET MVC: How can I get the browser to open and display a PDF instead of displaying a download prompt?

Ok, so I have an action method that generates a PDF and returns it to the browser. The problem is that instead of automatically opening the PDF, IE displays a download prompt even though it knows what kind of file it is. Chrome does the same thing. In both browsers if I click a link to a PDF file that is stored on a server it will open up just fine and never display a download prompt.
Here is the code that is called to return the PDF:
public FileResult Report(int id)
{
var customer = customersRepository.GetCustomer(id);
if (customer != null)
{
return File(RenderPDF(this.ControllerContext, "~/Views/Forms/Report.aspx", customer), "application/pdf", "Report - Customer # " + id.ToString() + ".pdf");
}
return null;
}
Here's the response header from the server:
HTTP/1.1 200 OK
Server: ASP.NET Development Server/10.0.0.0
Date: Thu, 16 Sep 2010 06:14:13 GMT
X-AspNet-Version: 4.0.30319
X-AspNetMvc-Version: 2.0
Content-Disposition: attachment; filename="Report - Customer # 60.pdf"
Cache-Control: private, s-maxage=0
Content-Type: application/pdf
Content-Length: 79244
Connection: Close
Do I have to add something special to the response to get the browser to open the PDF automatically?
Any help is greatly appreciated! Thanks!
Response.AppendHeader("Content-Disposition", "inline; filename=foo.pdf");
return File(...
On the HTTP level your 'Content-Disposition' header should have 'inline' not 'attachment'.
Unfortunately, that's not supported by the FileResult (or it's derived classes) directly.
If you're already generating the document in a page or handler you could simply redirect the browser there. If that's not what you want you could subclass the FileResult and add support for streaming documents inline.
public class CustomFileResult : FileContentResult
{
public CustomFileResult( byte[] fileContents, string contentType ) : base( fileContents, contentType )
{
}
public bool Inline { get; set; }
public override void ExecuteResult( ControllerContext context )
{
if( context == null )
{
throw new ArgumentNullException( "context" );
}
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = ContentType;
if( !string.IsNullOrEmpty( FileDownloadName ) )
{
string str = new ContentDisposition { FileName = this.FileDownloadName, Inline = Inline }.ToString();
context.HttpContext.Response.AddHeader( "Content-Disposition", str );
}
WriteFile( response );
}
}
A simpler solution is not to specify the filename on the Controller.File method. This way you will not get the ContentDisposition header, which means you loose the file name hint when saving the PDF.
I had same issue,but none of the solutions not worked in Firefox until I changed the Options of my browser. In Options
window,then Application Tab change the Portable Document Format to Preview in Firefox.
I use following classes for having more options with content-disposition header.
It works quite like Marnix answer, but instead of fully generating the header with the ContentDisposition class, which unfortunately does not comply to RFC when file name has to be utf-8 encoded, it tweaks instead the header generated by MVC, which complies to RFC.
(Originally, I have written that in part using this response to another question and this another one.)
using System;
using System.IO;
using System.Web;
using System.Web.Mvc;
namespace Whatever
{
/// <summary>
/// Add to FilePathResult some properties for specifying file name without forcing a download and specifying size.
/// And add a workaround for allowing error cases to still display error page.
/// </summary>
public class FilePathResultEx : FilePathResult
{
/// <summary>
/// In case a file name has been supplied, control whether it should be opened inline or downloaded.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool Inline { get; set; }
/// <summary>
/// Whether file size should be indicated or not.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool IncludeSize { get; set; }
public FilePathResultEx(string fileName, string contentType) : base(fileName, contentType) { }
public override void ExecuteResult(ControllerContext context)
{
FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
}
protected override void WriteFile(HttpResponseBase response)
{
if (Inline)
FileResultUtils.TweakDispositionAsInline(response);
// File.Exists is more robust than testing through FileInfo, especially in case of invalid path: it does yield false rather than an exception.
// We wish not to crash here, in order to let FilePathResult crash in its usual way.
if (IncludeSize && File.Exists(FileName))
{
var fileInfo = new FileInfo(FileName);
FileResultUtils.TweakDispositionSize(response, fileInfo.Length);
}
base.WriteFile(response);
}
}
/// <summary>
/// Add to FileStreamResult some properties for specifying file name without forcing a download and specifying size.
/// And add a workaround for allowing error cases to still display error page.
/// </summary>
public class FileStreamResultEx : FileStreamResult
{
/// <summary>
/// In case a file name has been supplied, control whether it should be opened inline or downloaded.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool Inline { get; set; }
/// <summary>
/// If greater than <c>0</c>, the content size to include in content-disposition header.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public long Size { get; set; }
public FileStreamResultEx(Stream fileStream, string contentType) : base(fileStream, contentType) { }
public override void ExecuteResult(ControllerContext context)
{
FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
}
protected override void WriteFile(HttpResponseBase response)
{
if (Inline)
FileResultUtils.TweakDispositionAsInline(response);
FileResultUtils.TweakDispositionSize(response, Size);
base.WriteFile(response);
}
}
/// <summary>
/// Add to FileContentResult some properties for specifying file name without forcing a download and specifying size.
/// And add a workaround for allowing error cases to still display error page.
/// </summary>
public class FileContentResultEx : FileContentResult
{
/// <summary>
/// In case a file name has been supplied, control whether it should be opened inline or downloaded.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool Inline { get; set; }
/// <summary>
/// Whether file size should be indicated or not.
/// </summary>
/// <remarks>If <c>FileDownloadName</c> is <c>null</c> or empty, this property has no effect (due to current implementation).</remarks>
public bool IncludeSize { get; set; }
public FileContentResultEx(byte[] fileContents, string contentType) : base(fileContents, contentType) { }
public override void ExecuteResult(ControllerContext context)
{
FileResultUtils.ExecuteResultWithHeadersRestoredOnFailure(context, base.ExecuteResult);
}
protected override void WriteFile(HttpResponseBase response)
{
if (Inline)
FileResultUtils.TweakDispositionAsInline(response);
if (IncludeSize)
FileResultUtils.TweakDispositionSize(response, FileContents.LongLength);
base.WriteFile(response);
}
}
public static class FileResultUtils
{
public static void ExecuteResultWithHeadersRestoredOnFailure(ControllerContext context, Action<ControllerContext> executeResult)
{
if (context == null)
throw new ArgumentNullException("context");
if (executeResult == null)
throw new ArgumentNullException("executeResult");
var response = context.HttpContext.Response;
var previousContentType = response.ContentType;
try
{
executeResult(context);
}
catch
{
if (response.HeadersWritten)
throw;
// Error logic will usually output a content corresponding to original content type. Restore it if response can still be rewritten.
// (Error logic should ensure headers positionning itself indeed... But this is not the case at least with HandleErrorAttribute.)
response.ContentType = previousContentType;
// If a content-disposition header have been set (through DownloadFilename), it must be removed too.
response.Headers.Remove(ContentDispositionHeader);
throw;
}
}
private const string ContentDispositionHeader = "Content-Disposition";
// Unfortunately, the content disposition generation logic is hidden in an Mvc.Net internal class, while not trivial (UTF-8 support).
// Hacking it after its generation.
// Beware, do not try using System.Net.Mime.ContentDisposition instead, it does not conform to the RFC. It does some base64 UTF-8
// encoding while it should append '*' to parameter name and use RFC 5987 encoding. https://www.rfc-editor.org/rfc/rfc6266#section-4.3
// And https://stackoverflow.com/a/22221217/1178314 comment.
// To ask for a fix: https://github.com/aspnet/Mvc
// Other class : System.Net.Http.Headers.ContentDispositionHeaderValue looks better. But requires to detect if the filename needs encoding
// and if yes, use the 'Star' suffixed property along with setting the sanitized name in non Star property.
// MVC 6 relies on ASP.NET 5 https://github.com/aspnet/HttpAbstractions which provide a forked version of previous class, with a method
// for handling that: https://github.com/aspnet/HttpAbstractions/blob/dev/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs
// MVC 6 stil does not give control on FileResult content-disposition header.
public static void TweakDispositionAsInline(HttpResponseBase response)
{
var disposition = response.Headers[ContentDispositionHeader];
const string downloadModeToken = "attachment;";
if (string.IsNullOrEmpty(disposition) || !disposition.StartsWith(downloadModeToken, StringComparison.OrdinalIgnoreCase))
return;
response.Headers.Remove(ContentDispositionHeader);
response.Headers.Add(ContentDispositionHeader, "inline;" + disposition.Substring(downloadModeToken.Length));
}
public static void TweakDispositionSize(HttpResponseBase response, long size)
{
if (size <= 0)
return;
var disposition = response.Headers[ContentDispositionHeader];
const string sizeToken = "size=";
// Due to current ancestor semantics (no file => inline, file name => download), handling lack of ancestor content-disposition
// is non trivial. In this case, the content is by default inline, while the Inline property is <c>false</c> by default.
// This could lead to an unexpected behavior change. So currently not handled.
if (string.IsNullOrEmpty(disposition) || disposition.Contains(sizeToken))
return;
response.Headers.Remove(ContentDispositionHeader);
response.Headers.Add(ContentDispositionHeader, disposition + "; " + sizeToken + size.ToString());
}
}
}
Sample usage:
public FileResult Download(int id)
{
// some code to get filepath and filename for browser
...
return
new FilePathResultEx(filepath, System.Web.MimeMapping.GetMimeMapping(filename))
{
FileDownloadName = filename,
Inline = true
};
}
Note that specifying a file name with Inline will not work with Internet Explorer (11 included, Windows 10 Edge included, tested with some pdf files), while it works with Firefox and Chrome. Internet Explorer will ignore the file name. For Internet Explorer, you need to hack your url path, which is quite bad imo. See this answer.
Just return a FileStreamResult instead of File
And make sure you don't wrap your new FileStreamResult in a File at the end. Just return the FileStreamResult as it is. And probably you need to modify the return type of the action also to FileSteamResult

Resources