Orchard Tokens -> - orchardcms

Im getting a headache from the Orchard Token Providers and any help would be a blessing.
Ive mostly been copying the Comments Tokens.
The target is for an email to be sent when a 'Question' content item is published.
Here is my solution for the Content Type 'Question':
public interface ITokenProvider : IEventHandler
{
void Describe(dynamic context);
void Evaluate(dynamic context);
}
public class QuestionTokens : ITokenProvider
{
public QuestionTokens()
{
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(dynamic context)
{
//presume this is correct
context.For("Content", T("Content Items"), T("Content Items"))
.Token("QuestionText", T("Question Text"), T("Text of the question"))
.Token("QuestionAuthor", T("Question Author"), T("Author of the question"));
//presume this is incorrect? correct for the content type?
context.For("Question", T("Questions"), T("Questions from users"))
.Token("QuestionText", T("Question Text"), T("Text of the question"))
.Token("QuestionAuthor", T("Question Author"), T("Author of the question"));
}
public void Evaluate(dynamic context)
{
Func<IContent, object> questionTextAccessorFromContent = (content) => {
var part = content.As<QuestionPart>();
return part.QuestionText;
};
Func<QuestionPart, object> questionTextAccessor = (part) =>
{
return part.QuestionText;
};
Func<IContent, object> authorAccessorFromContent = (content) => {
var part = content.As<QuestionPart>();
return part.Author;
};
Func<QuestionPart, object> authorAccessor = (part) =>
{
return part.Author;
};
//doesnt work
context.For<IContent>("Content")
.Token("QuestionText", (Func<IContent, object>)
(content => content.As<QuestionPart>().Record.QuestionText))
.Token("QuestionAuthor", (Func<IContent, object>)
(content => content.As<QuestionPart>().Record.Author));
//doesnt work
context.For<IContent>("Question")
.Token("QuestionText", (Func<IContent, object>)
(content => content.As<QuestionPart>().Record.QuestionText))
.Token("QuestionAuthor", (Func<IContent, object>)
(content => content.As<QuestionPart>().Record.Author));
//doesnt work
context.For<QuestionPart>("Question")
.Token("QuestionText", (Func<QuestionPart, object>)
(content => content.Record.QuestionText))
.Token("Author", (Func<QuestionPart, object>)
(content => content.Record.Author)); ;
}
private static string QuestionText(IContent question)
{
var questionPart = question.As<QuestionPart>();
return questionPart.QuestionText;
}
private static string Author(IContent question)
{
var questionPart = question.As<QuestionPart>();
return questionPart.Author;
}
}
This is the Action (when the Question is published), it sends a email with the body text:
(updated to test Medeiros suggestion)
<p>A question has been created by: {Content.QuestionAuthor}</p>
<p>A question has been created by: {Content.QuestionAuthor.Text}</p>
<p>A question has been created by: {Content.QuestionAuthor.Value}</p>
<p>A question has been created by: {Question.QuestionAuthor}</p>
<p>A question has been created by: {Question.QuestionAuthor.Text}</p>
<p>A question has been created by: {Question.QuestionAuthor.Value}</p>
<p>Message: {Content.QuestionText}</p>
<p>Message: {Content.QuestionText.Text}</p>
<p>Message: {Content.QuestionText.Value}</p>
<p>Message: {Question.QuestionText}</p>
<p>Message: {Question.QuestionText.Text}</p>
<p>Message: {Question.QuestionText.Value}</p>
All 'my' tokens are replaced with blank text. Other tokens like: {Content.ContentType} {User.Email} work just fine. Any mistakes or hints that anyone notices will be very useful.
Thanks, Matt

IVe re-written the token provider and it works sort of... (and now works completely)
public class QuestionTokens : Orchard.Tokens.ITokenProvider
{
private readonly IContentManager contentManager;
private readonly IWorkContextAccessor workContextAccessor;
public QuestionTokens(IContentManager contentManager, IWorkContextAccessor workContextAccessor)
{
this.workContextAccessor = workContextAccessor;
this.contentManager = contentManager;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
/// <summary>
/// Describes the specified context.
/// </summary>
/// <param name="context">The context.</param>
public void Describe(DescribeContext context)
{
context.For("Content", T("Content Items"), T("Content Items"))
.Token("QuestionText", T("Question Text"), T("Text of the question"))
.Token("QuestionAuthor", T("Question Author"), T("Author of the question"));
}
/// <summary>
/// Evaluates the specified context.
/// </summary>
/// <param name="context">The context.</param>
public void Evaluate(EvaluateContext context)
{
context.For<IContent>("Content")
.Token("QuestionText", content => QuestionText(content, context))
.Token("QuestionAuthor", content => Author(content, context));
}
private string QuestionText(IContent question, EvaluateContext context)
{
var questionPart = question.As<QuestionPart>();
return questionPart.QuestionText;
}
private string Author(IContent question, EvaluateContext context)
{
var questionPart = question.As<QuestionPart>();
return questionPart.Author;
}
}
I can now debug the code at least in the private QuestionText and Author methods. The value retrieved is 'null' due to the published event firing when this line is fired in the controller:
services.ContentManager.Create("Question");
so the content item has not been populated and the 'published' event is firing before its life has begun. Time to play about with its content type settings a bit to see if that makes a difference...
Finally all working as needed. The Events section 'published' event is still happening when i dont expect it to so I've changed the life of the content.
From a controller: (some code into a service removed so I don't have a post more code)
var item = services.ContentManager.New<Models.QuestionPart>("Question");
this.TryUpdateModel(item);
services.ContentManager.Create(item.ContentItem); //where the event is firing
Services.ContentManager.Publish(item.ContentItem); //where i expected the event to fire :)
Originally it was create, update and save. But the events restricted that. So now its new, update and create.
As for the tokens. They are simply:
{Content.QuestionAuthor} and {Content.QuestionText}. Nice and simple when everything is working, and everything is right.

Matthew, try adding a .Value for the: {Content.QuestionAuthor.Value}
If this doesn't work, try .Text.
Hope this helps.

The code in your original question looks accurate. You follow the correct Orchard Events bus convention of creating an interface inheriting from IEventHandler link.
Your change in the controller code is likely the reason you are getting values. My experience debugging the code is that it depends on what Event your rule is subscribing to. For example an event of type "Content Created" hits the debugger in the Evaluate method but all custom part properties are null. But an event of "When a Custom Form is submitted" hits and has values populated.
I spent a full day troubleshooting my code which looks similar to yours above and it wasn't until I started testing the other event types in my rule that it had data populated in the private functions.
Here's my code:
public interface ITokenProvider : IEventHandler
{
void Describe(dynamic context);
void Evaluate(dynamic context);
}
public class PersonTokens : ITokenProvider
{
private readonly IContentManager _contentManager;
public Localizer T { get; set; }
public PersonTokens(IContentManager contentManager)
{
_contentManager = contentManager;
T = NullLocalizer.Instance;
}
public void Describe(dynamic context)
{
context.For("Content", T("Content Items"), T("Content Items"))
.Token("FirstName", T("First Name"), T("The Person's First Name"))
.Token("LastName", T("Last Name"), T("The Person's Last Name"))
.Token("EmailAddress", T("Email Address"), T("The Person's Email Address"))
.Token("PhoneNumber", T("Phone Number"), T("The Person's Phone Number"));
}
public void Evaluate(dynamic context)
{
// Orchard.Tokens.Implementation.TokenManager.EvaluateContextImpl
context.For<IContent>("Content")
.Token("FirstName", (Func<IContent, object>)FirstName)
.Token("LastName", (Func<IContent, object>)(content => content.As<PersonPart>().Record.LastName))
.Token("EmailAddress", (Func<IContent, object>)(content => content.As<PersonPart>().EmailAddress))
.Token("PhoneNumber", (Func<IContent, object>)PhoneNumber); // left the PhoneNumber out of sample
}
private string FirstName(IContent person)
{
var personPart = person.As<PersonPart>();
return personPart.Record.FirstName;
}
}

Related

Overriding PXFilteredProcessingJoin and delegate for APPrintChecks

I have the same issue as the below link but with a different graph (APPrintChecks)
how-do-i-override-pxfilteredprocessingjoin-in-a-graph-extension-without-altering
I am overriding the main view to pull in the remittance name from APContact to show in the grid.
[PXFilterable]
public PXFilteredProcessingJoin<APPayment, PrintChecksFilter,
InnerJoin<Vendor, On<Vendor.bAccountID, Equal<APPayment.vendorID>>,
InnerJoin<APContact, On<APContact.contactID, Equal<APPayment.remitContactID>>>>,
Where<boolTrue, Equal<boolTrue>>,
OrderBy<Asc<Vendor.acctName, Asc<APPayment.refNbr>>>> APPaymentList;
However, I do not know how to override the delegate so I won't have the same problem as the other poster (no filter being applied).
protected virtual IEnumerable appaymentlist()
{
if (cleared)
{
foreach (APPayment doc in APPaymentList.Cache.Updated)
{
doc.Passed = false;
}
}
foreach (PXResult<APPayment, Vendor, PaymentMethod, CABatchDetail> doc in PXSelectJoin<APPayment,
InnerJoinSingleTable<Vendor, On<Vendor.bAccountID, Equal<APPayment.vendorID>>,
InnerJoin<PaymentMethod, On<PaymentMethod.paymentMethodID, Equal<APPayment.paymentMethodID>>,
LeftJoin<CABatchDetail, On<CABatchDetail.origModule, Equal<BatchModule.moduleAP>,
And<CABatchDetail.origDocType, Equal<APPayment.docType>,
And<CABatchDetail.origRefNbr, Equal<APPayment.refNbr>>>>>>>,
Where2<Where<APPayment.status, Equal<APDocStatus.pendingPrint>,
And<CABatchDetail.batchNbr, IsNull,
And<APPayment.cashAccountID, Equal<Current<PrintChecksFilter.payAccountID>>,
And<APPayment.paymentMethodID, Equal<Current<PrintChecksFilter.payTypeID>>,
And<Match<Vendor, Current<AccessInfo.userName>>>>>>>,
And<APPayment.docType, In3<APDocType.check, APDocType.prepayment, APDocType.quickCheck>>>>.Select(this))
{
yield return new PXResult<APPayment, Vendor>(doc, doc);
if (_copies.ContainsKey((APPayment)doc))
{
_copies.Remove((APPayment)doc);
}
_copies.Add((APPayment)doc, PXCache<APPayment>.CreateCopy(doc));
}
}
There are other private variables that are referenced in this. Any help appreciated.
Also, if there's a simpler way to pull in a related value on a grid like this (virtual field in DAC?) I'm not stuck on doing it with a graph extension.
So this appears to work but it seems messy and duplicates a lot of code and private variables. Appreciate any feedback if there's a better way to do this:
public class APPrintChecks_Extension : PXGraphExtension<APPrintChecks> {
[PXFilterable]
public PXFilteredProcessingJoin<APPayment, PrintChecksFilter,
InnerJoin<Vendor, On<Vendor.bAccountID, Equal<APPayment.vendorID>>,
InnerJoin<APContact, On<APContact.contactID, Equal<APPayment.remitContactID>>>>,
Where<boolTrue, Equal<boolTrue>>,
OrderBy<Asc<Vendor.acctName, Asc<APPayment.refNbr>>>> APPaymentList;
public IEnumerable appaymentlist()
{
if (cleared)
{
foreach (APPayment doc in APPaymentList.Cache.Updated)
{
doc.Passed = false;
}
}
foreach (PXResult<APPayment, Vendor, APContact, PaymentMethod, CABatchDetail> doc in PXSelectJoin<APPayment,
InnerJoinSingleTable<Vendor, On<Vendor.bAccountID, Equal<APPayment.vendorID>>,
InnerJoin<APContact, On<APContact.contactID, Equal<APPayment.remitContactID>>,
InnerJoin<PaymentMethod, On<PaymentMethod.paymentMethodID, Equal<APPayment.paymentMethodID>>,
LeftJoin<CABatchDetail, On<CABatchDetail.origModule, Equal<BatchModule.moduleAP>,
And<CABatchDetail.origDocType, Equal<APPayment.docType>,
And<CABatchDetail.origRefNbr, Equal<APPayment.refNbr>>>>>>>>,
Where2<Where<APPayment.status, Equal<APDocStatus.pendingPrint>,
And<CABatchDetail.batchNbr, IsNull,
And<APPayment.cashAccountID, Equal<Current<PrintChecksFilter.payAccountID>>,
And<APPayment.paymentMethodID, Equal<Current<PrintChecksFilter.payTypeID>>,
And<Match<Vendor, Current<AccessInfo.userName>>>>>>>,
And<APPayment.docType, In3<APDocType.check, APDocType.prepayment, APDocType.quickCheck>>>>.Select(Base))
{
yield return new PXResult<APPayment, Vendor, APContact>(doc, doc, doc);
if (_copies.ContainsKey((APPayment)doc))
{
_copies.Remove((APPayment)doc);
}
_copies.Add((APPayment)doc, PXCache<APPayment>.CreateCopy(doc));
}
}
private bool cleared;
public void Clear()
{
Base.Filter.Current.CurySelTotal = 0m;
Base.Filter.Current.SelTotal = 0m;
Base.Filter.Current.SelCount = 0;
cleared = true;
Base.Clear();
}
private readonly Dictionary<object, object> _copies = new Dictionary<object, object>();
}
Per Rick's suggestion I implemented the FieldSelecting method. Much, much simpler/cleaner code. It does in fact cause a round trip to the database for each row when using this in a grid column, however, for check printing this should be acceptable. Thanks Rick! Code below.
protected void APPayment_UsrRemitTo_FieldSelecting(PXCache cache, PXFieldSelectingEventArgs e)
{
var row = (APPayment)e.Row;
// fill usrRemitTo from APContact
if (row != null)
{
var extension = PXCache<APRegister>.GetExtension<APRegisterExt>(row);
using (PXConnectionScope cs = new PXConnectionScope())
{
APContact rec = PXSelectReadonly<APContact, Where<APContact.contactID, Equal<Required<APPayment.remitContactID>>>>.Select(Base, row.RemitContactID);
if (rec != null)
{
string remitToName = (!string.IsNullOrEmpty(rec.FullName)) ? rec.FullName : "";
e.ReturnValue = remitToName;
}
else
{
e.ReturnValue = "";
}
}
}
}
Using DBScalar, you can further simplify your event with just one line :
public class APPaymentExt : PXCacheExtension<APPayment>
{
#region UsrRemitTo
[PXString(100)]
[PXUIField(DisplayName="Remit To")]
[PXDBScalar(typeof(Search<APContact.fullName,Where<APContact.contactID, Equal<APPayment.remitContactID>>>))]
public virtual string UsrRemitTo { get; set; }
public abstract class usrRemitTo : PX.Data.BQL.BqlString.Field<usrRemitTo> { }
#endregion
}

How to implement a System.Reactive scheduler for ios

I'm using System.Reactive within my ios project and i'm aware i need to use ObserveOn in order to specify on which thread to execute the subscriber on. However i can't seem to get this working properly.
For all i can tell this should be working, or am i implementing it wrong?
public class UiContext : IScheduler
{
/// <inheritdoc />
public IDisposable Schedule<TState>(TState state, Func<IScheduler, TState, IDisposable> action)
{
NSOperationQueue.MainQueue.AddOperation(() => action(this, state));
return Disposable.Empty;
}
/// <inheritdoc />
public IDisposable Schedule<TState>(TState state, TimeSpan dueTime, Func<IScheduler, TState, IDisposable> action)
{
NSOperationQueue.MainQueue.AddOperation(() => action(this, state));
return Disposable.Empty;
}
/// <inheritdoc />
public IDisposable Schedule<TState>(TState state, DateTimeOffset dueTime, Func<IScheduler, TState, IDisposable> action)
{
NSOperationQueue.MainQueue.AddOperation(() => action(this, state));
return Disposable.Empty;
}
/// <inheritdoc />
public DateTimeOffset Now { get; }
}
void SomeMethod()
{
WhenValidationChanged
.ObserveOn(new UiContext())
.SubscribeOn(new UiContext())
.Throttle(TimeSpan.FromMilliseconds(50))
.Subscribe(OnValidationChanged);
}
private void OnValidationChanged(object obj)
{
if (TableView.DataSource is InfoFieldsDataSource dataSource)
{
var validationErrors = dataSource.Items.OfType<InfoFieldViewModelBase>().Count(d => !d.IsValid);
// Exception is raised about not being executed on UI thread
_validationController.View.BackgroundColor = validationErrors > 0 ? UIColor.Green : UIColor.Red;
}
}
Calling .ObserveOn(new UiContext()) before .Throttle(TimeSpan.FromMilliseconds(50)) probably has no effect as Throttle can change the scheduler - each operator can change the scheduler. You should always do .ObserveOn just before the operator or subscribe call you want it applied to.

VaryByParam fails if a param is a list

I've got this action in MVC
[OutputCache(Duration = 1200, VaryByParam = "*")]
public ActionResult FilterArea( string listType, List<int> designersID, int currPage = 1 )
{
// Code removed
}
that fails to present the correct HTML with url like
http://example.com/en-US/women/clothing?designersID=158
http://example.com/en-US/women/clothing?designersID=158&designersID=13
Is this a know bug of OutputCache in .NET cause cannot recognize VaryByParam with a list param or am I missing something?
I too had the same issue in MVC3 and I believe it's still the same case in MVC5.
Here is the setup I had.
Request
POST, Content-Type:application/json, passing in an array of string as the parameter
{ "options": ["option1", "option2"] }
Controller Method
[OutputCache(Duration = 3600, Location = OutputCacheLocation.Any, VaryByParam = "options")]
public ActionResult GetOptionValues(List<string> options)
I tried every option possible with OutputCache and it just wasn't caching for me. Binding worked fine for the actual method to work. My biggest suspicion was that OutputCache wasn't creating unique cache keys so I even pulled its code out of System.Web.MVC.OutputCache to verify. I've verified that it properly builds unique keys even when a List<string> is passed in. Something else is buggy in there but wasn't worth spending more effort.
OutputCacheAttribute.GetUniqueIdFromActionParameters(filterContext,
OutputCacheAttribute.SplitVaryByParam(this.VaryByParam);
Workaround
I ended up creating my own OutputCache attribute following another SO post. Much easier to use and I can go enjoy the rest of the day.
Controller Method
[MyOutputCache(Duration=3600)]
public ActionResult GetOptionValues(Options options)
Custom Request class
I've inherited from List<string> so I can call the overriden .ToString() method in MyOutputcache class to give me a unique cache key string. This approach alone has resolved similar issues for others but not for me.
[DataContract(Name = "Options", Namespace = "")]
public class Options: List<string>
{
public override string ToString()
{
var optionsString= new StringBuilder();
foreach (var option in this)
{
optionsString.Append(option);
}
return optionsString.ToString();
}
}
Custom OutputCache class
public class MyOutputCache : ActionFilterAttribute
{
private string _cachedKey;
public int Duration { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.Url != null)
{
var path = filterContext.HttpContext.Request.Url.PathAndQuery;
var attributeNames = filterContext.ActionParameters["Options"] as AttributeNames;
if (attributeNames != null) _cachedKey = "MYOUTPUTCACHE:" + path + attributeNames;
}
if (filterContext.HttpContext.Cache[_cachedKey] != null)
{
filterContext.Result = (ActionResult) filterContext.HttpContext.Cache[_cachedKey];
}
else
{
base.OnActionExecuting(filterContext);
}
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
filterContext.HttpContext.Cache.Add(_cachedKey, filterContext.Result, null,
DateTime.Now.AddSeconds(Duration), System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.Default, null);
base.OnActionExecuted(filterContext);
}
}

ViewModel property sort of fatal with VMDisconnectedException

EDIT 2: If you're looking for an answer to a similar problem, check Stuart's answer and my comments on it.
EDIT: I am actually getting a Mono.Debugger.Soft.VMDisconnectedException. I also recently installed Windows 8.1 and Resharper (though Resharper is suspended now).
When I access a very simple list property of my view model in my MVVMCross Xamarin iOS application, the program fails. It doesn't quit most of the time: it acts like it's running. The simulator has a black screen and there is no exception. If I breakpoint on if (messagesViewModel != null) source.ItemsSource = messagesViewModel.Messages; and then type messagesViewModel.Messages into the Immediate Window, everything stops, so I can tell it is failing at this line. If instead I "step over", it never moves to the next line.
I was having similar behavior when I was toggling this code in the MvxTableViewSource:
public override int RowsInSection(UITableView tableview, int section)
{
return 1;
}
My view model looks like this:
public class MessagesViewModel : MvxViewModel
{
private List<BaseMessage> _messages = null;
public List<BaseMessage> Messages
{
get
{
return _messages; //yes, I know I'm returning null
//I wasn't at first.
}
}
public MessagesViewModel()
{
}
}
This is my ViewDIdLoad on the MvxTableViewController:
public override void ViewDidLoad()
{
base.ViewDidLoad();
var source = new MessagesTableViewSource(TableView);
//was binding here, removed it for debug purposes
//failure on second line here
var messagesViewModel = ViewModel as MessagesViewModel;
if (messagesViewModel != null) source.ItemsSource = messagesViewModel.Messages;
TableView.Source = source;
TableView.ReloadData();
}
Some initialization code:
public class App : MvxApplication
{
public App()
{
var appStart = new MvxAppStart<MessagesViewModel>();
Mvx.RegisterSingleton<IMvxAppStart>(appStart);
}
}
public partial class AppDelegate : MvxApplicationDelegate
{
//empty functions removed.
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
Window = new UIWindow(UIScreen.MainScreen.Bounds);
var presenter = new MvxTouchViewPresenter(this, Window);
var setup = new Setup(this, presenter);
setup.Initialize();
var startup = Mvx.Resolve<IMvxAppStart>();
startup.Start();
Window.MakeKeyAndVisible();
return true;
}
}
I suspect whatever the error is, it isn't in any of the code you have posted.
I just created a simple ViewModel:
public class FirstViewModel
: MvxViewModel
{
private List<string> _items = new List<string>() { "One", "Two", "Three"};
public List<string> Items
{
get { return _items; }
set { _items = value; RaisePropertyChanged(() => Items); }
}
}
And a simple View:
[Register("FirstView")]
public class FirstView : MvxTableViewController
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
// ios7 layout
if (RespondsToSelector(new Selector("edgesForExtendedLayout")))
EdgesForExtendedLayout = UIRectEdge.None;
var firstViewModel = ViewModel as FirstViewModel;
var source = new MessagesTableViewSource(TableView);
source.ItemsSource = firstViewModel.Items;
TableView.Source = source;
}
public class MessagesTableViewSource : MvxTableViewSource
{
public MessagesTableViewSource(UITableView tableView) : base(tableView)
{
tableView.RegisterClassForCellReuse(typeof(MessagesCell), new NSString("MessagesCell"));
}
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
return tableView.DequeueReusableCell("MessagesCell");
}
}
public class MessagesCell : MvxTableViewCell
{
public MessagesCell(IntPtr handle)
: base(handle)
{
var txt = new UILabel(new RectangleF(0, 0, 320, 44));
Add(txt);
this.DelayBind(() =>
{
this.CreateBinding(txt).Apply();
});
}
}
}
And this code runs fine...
I wouldn't completely trust the integration of Xamarin.iOS with the Immediate window - it is better now than it used to be, but I've seen several problems with it before.
Some things to possibly check:
does the above code work for you?
if it does, then what's in your BaseMessage and MessagesTableViewSource classes - perhaps they are causing the problem?
can you use Mvx.Trace("The list is {0}", messagesViewModel.Messages ?? "-null") to view the list? Can you use trace within the ViewModel property get - is it being called? Can you use trace within the ViewModel constructor?
are all your assemblies building against the same versions of things? Are all your assemblies definitely rebuilt? (Check "Build|Configuration Manager")- what version of Xamarin.iOS are you running in VS and in the Mac?

How does one correctly implement a MediaTypeFormatter to handle requests of type 'multipart/mixed'?

Consider a web service written in ASP.NET Web API to accept any number files as a 'multipart/mixed' request. The helper method mat look as follows (assuming _client is an instance of System.Net.Http.HttpClient):
public T Post<T>(string requestUri, T value, params Stream[] streams)
{
var requestMessage = new HttpRequestMessage();
var objectContent = requestMessage.CreateContent(
value,
MediaTypeHeaderValue.Parse("application/json"),
new MediaTypeFormatter[] {new JsonMediaTypeFormatter()},
new FormatterSelector());
var content = new MultipartContent();
content.Add(objectContent);
foreach (var stream in streams)
{
var streamContent = new StreamContent(stream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
streamContent.Headers.ContentDisposition =
new ContentDispositionHeaderValue("form-data")
{
Name = "file",
FileName = "mystream.doc"
};
content.Add(streamContent);
}
return _httpClient.PostAsync(requestUri, content)
.ContinueWith(t => t.Result.Content.ReadAsAsync<T>()).Unwrap().Result;
}
The method that accepts the request in the subclass of ApiController has a signature as follows:
public HttpResponseMessage Post(HttpRequestMessage request)
{
/* parse request using MultipartFormDataStreamProvider */
}
Ideally, I'd like to define it like this, where contact, source and target are extracted from the 'multipart/mixed' content based on the 'name' property of the 'Content-Disposition' header.
public HttpResponseMessage Post(Contact contact, Stream source, Stream target)
{
// process contact, source and target
}
However, with my existing signature, posting the data to the server results in an InvalidOperationException with an error message of:
No 'MediaTypeFormatter' is available to read an object of type
'HttpRequestMessage' with the media type 'multipart/mixed'.
There are a number of examples on the internet how to send and receive files using the ASP.NET Web API and HttpClient. However, I have not found any that show how to deal with this problem.
I started looking at implementing a custom MediaTypeFormatter and register it with the global configuration. However, while it is easy to deal with serializing XML and JSON in a custom MediaTypeFormatter, it is unclear how to deal with 'multipart/mixed' requests which can pretty much be anything.
Have a look at this forum: http://forums.asp.net/t/1777847.aspx/1?MVC4+Beta+Web+API+and+multipart+form+data
Here is a snippet of code (posted by imran_ku07) that might help you implement a custom formatter to handle the multipart/form-data:
public class MultiFormDataMediaTypeFormatter : FormUrlEncodedMediaTypeFormatter
{
public MultiFormDataMediaTypeFormatter() : base()
{
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data"));
}
protected override bool CanReadType(Type type)
{
return true;
}
protected override bool CanWriteType(Type type)
{
return false;
}
protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext)
{
var contents = formatterContext.Request.Content.ReadAsMultipartAsync().Result;
return Task.Factory.StartNew<object>(() =>
{
return new MultiFormKeyValueModel(contents);
});
}
class MultiFormKeyValueModel : IKeyValueModel
{
IEnumerable<HttpContent> _contents;
public MultiFormKeyValueModel(IEnumerable<HttpContent> contents)
{
_contents = contents;
}
public IEnumerable<string> Keys
{
get
{
return _contents.Cast<string>();
}
}
public bool TryGetValue(string key, out object value)
{
value = _contents.FirstDispositionNameOrDefault(key).ReadAsStringAsync().Result;
return true;
}
}
}
You then need to add this formatter to your application. If doing self-host you can simply add it by including:
config.Formatters.Insert(0, new MultiFormDataMediaTypeFormatter());
before instantiating the HttpSelfHostServer class.
-- EDIT --
To parse binary streams you'll need another formatter. Here is one that I am using to parse images in one of my work projects.
class JpegFormatter : MediaTypeFormatter
{
protected override bool CanReadType(Type type)
{
return (type == typeof(Binary));
}
protected override bool CanWriteType(Type type)
{
return false;
}
public JpegFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpeg"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/jpg"));
SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png"));
}
protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext)
{
return Task.Factory.StartNew(() =>
{
byte[] fileBytes = new byte[stream.Length];
stream.Read(fileBytes, 0, (int)fileBytes.Length);
return (object)new Binary(fileBytes);
});
}
protected override Task OnWriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, FormatterContext formatterContext, TransportContext transportContext)
{
throw new NotImplementedException();
}
}
In your controller/action you'll want to do something along the lines of:
public HttpResponseMessage UploadImage(Binary File) {
//do something with your file
}
Take a look at this post https://stackoverflow.com/a/17073113/1944993 the answer of Kiran Challa is really nice!
The essential part :
Custom In-memory MultiaprtFormDataStreamProvider:
public class InMemoryMultipartFormDataStreamProvider : MultipartStreamProvider
{
private NameValueCollection _formData = new NameValueCollection();
private List<HttpContent> _fileContents = new List<HttpContent>();
// Set of indexes of which HttpContents we designate as form data
private Collection<bool> _isFormData = new Collection<bool>();
/// <summary>
/// Gets a <see cref="NameValueCollection"/> of form data passed as part of the multipart form data.
/// </summary>
public NameValueCollection FormData
{
get { return _formData; }
}
/// <summary>
/// Gets list of <see cref="HttpContent"/>s which contain uploaded files as in-memory representation.
/// </summary>
public List<HttpContent> Files
{
get { return _fileContents; }
}
public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
{
// For form data, Content-Disposition header is a requirement
ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
if (contentDisposition != null)
{
// We will post process this as form data
_isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName));
return new MemoryStream();
}
// If no Content-Disposition header was present.
throw new InvalidOperationException(string.Format("Did not find required '{0}' header field in MIME multipart body part..", "Content-Disposition"));
}
/// <summary>
/// Read the non-file contents as form data.
/// </summary>
/// <returns></returns>
public override async Task ExecutePostProcessingAsync()
{
// Find instances of non-file HttpContents and read them asynchronously
// to get the string content and then add that as form data
for (int index = 0; index < Contents.Count; index++)
{
if (_isFormData[index])
{
HttpContent formContent = Contents[index];
// Extract name from Content-Disposition header. We know from earlier that the header is present.
ContentDispositionHeaderValue contentDisposition = formContent.Headers.ContentDisposition;
string formFieldName = UnquoteToken(contentDisposition.Name) ?? String.Empty;
// Read the contents as string data and add to form data
string formFieldValue = await formContent.ReadAsStringAsync();
FormData.Add(formFieldName, formFieldValue);
}
else
{
_fileContents.Add(Contents[index]);
}
}
}
/// <summary>
/// Remove bounding quotes on a token if present
/// </summary>
/// <param name="token">Token to unquote.</param>
/// <returns>Unquoted token.</returns>
private static string UnquoteToken(string token)
{
if (String.IsNullOrWhiteSpace(token))
{
return token;
}
if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1)
{
return token.Substring(1, token.Length - 2);
}
return token;
}}
You can then the "MemoryMultiPartDataStreamProvider" in you webapi like this :
var provider = await Request.Content.ReadAsMultipartAsync<InMemoryMultipartFormDataStreamProvider>(new InMemoryMultipartFormDataStreamProvider());
//access form data
NameValueCollection formData = provider.FormData;
//access files
IList<HttpContent> files = provider.Files;

Resources