There may be times when it's handy to prompt users to download a file after a slight delay. Such a delay can be useful because it gives users time to review a page's content before they are prompted to save. A good example of this technique can be found on SourceForge whenever a visitor attempts to download a project.
How can this be accomplished using ASP.NET MVC 3? Let's make a small use case to illustrate our goal and then dig into some sample code:
Load a "download" page.
After a 2 second delay, prompt the user to save a PDF document. (In this case we do not want the browser to use any plugins to render the document).
As per usual, Stack Overflow had a great post to get me started:
This code inserts an iframe into the DOM after a 2 second delay. This iframe is responsible for triggering the prompt to save the PDF. Unfortunately, I could not get this to work in ASP.NET MVC 3 by just referencing the path to the PDF in the source attribute of the iframe. To solve this, I added an action (and associated routing rules) to my Home controller for downloading the PDF as a FileContentResult.
public ActionResult DownloadPDF()
{
return File("~/FileToDownload.pdf", "application/pdf", "FileToDownload.pdf");
}
Including the third parameter in the "File" method adds the "Content-Disposition: attachment;" header - this forces the browser to handle the file using the save dialog prompt instead of any browser plugins.
To finish up, we'll revisit the jQuery code we started with and point the iframe to our new action instead of the direct path to the PDF.
In a recent MonoTouch project I needed to make something similar to the slide-to-unlock control at the bottom of the iOS lock screen.
The goal was to derive from a UIImageView and trigger an Activate event when a specified slider image got within range of the right end of the control. I found this was fairly straightforward to implement using a pan gesture. Below is the class I came up with and the associated images that were used:
Image
Slider
public class UISlideToActivateImageView : UIImageView
{
#region Fields and Properties
public event EventHandler<EventArgs> Activate;
protected const float DEFAULT_ACTIVATION_RANGE = 20;
public float ActivationRange { get; set; }
public static Selector PanSelector
{
get
{
return new Selector("HandlePan");
}
}
private UIImageView _sliderView = null;
public UIImage Slider
{
get
{
return _sliderView.Image;
}
set
{
if (value != null)
{
if (_sliderView != null)
{
_sliderView.RemoveFromSuperview();
}
_sliderView = new UIImageView(
new RectangleF(new PointF(0, 0), value.Size));
_sliderView.Image = value;
AddSubview(_sliderView);
}
}
}
protected PointF InitialLocation { get; set; }
#endregion
#region Constructors
public UISlideToActivateImageView(PointF location, UIImage image)
: base(image)
{
ActivationRange = DEFAULT_ACTIVATION_RANGE;
Frame = new RectangleF(location, image.Size);
RegisterPanGesture();
}
public UISlideToActivateImageView(PointF location, UIImage image,
UIImage slider) : base(image)
{
ActivationRange = DEFAULT_ACTIVATION_RANGE;
Frame = new RectangleF(location, image.Size);
Slider = slider;
RegisterPanGesture();
}
#endregion
#region Events, Overrides and Delegates
[Export("HandlePan")]
public void HandlePan(UIPanGestureRecognizer panGesture)
{
const double EndedAnimationDuration = 0.2d;
PointF newLocation;
float adjX;
if (panGesture != null)
{
newLocation = panGesture.LocationInView(this);
switch (panGesture.State)
{
case UIGestureRecognizerState.Began:
//User first taps the slider
if ((newLocation.X <= (Frame.X + Slider.Size.Width))
&& (newLocation.X >= 0))
{
InitialLocation = newLocation;
}
break;
case UIGestureRecognizerState.Changed:
//Moved their finger - make slider follow horizontal movements
adjX = Frame.X + (newLocation.X - InitialLocation.X);
if ((InitialLocation != PointF.Empty) && (adjX >= 0)
&& (adjX <= (Frame.Width - Slider.Size.Width)))
{
UIView.Animate(0d, delegate() {
_sliderView.Frame = new RectangleF(new PointF(adjX, 0),
_sliderView.Frame.Size);
});
//If the Slider comes within ActivationRange of end of this
//control, fire the Activate event
if ((Activate != null)
&& (adjX >=
(Frame.Width - Slider.Size.Width - ActivationRange)))
{
//Moved the slider all the way across the image view
Activate(this, EventArgs.Empty);
}
}
break;
case UIGestureRecognizerState.Cancelled:
case UIGestureRecognizerState.Failed:
case UIGestureRecognizerState.Ended:
//Lifted up finger - return slider to original position
InitialLocation = PointF.Empty;
UIView.Animate(EndedAnimationDuration, delegate() {
_sliderView.Frame = new RectangleF(new PointF(0, 0),
_sliderView.Frame.Size);
});
break;
}
}
}
//Delegate for allowing the pan gesture recognizer to receive touch.
public class ReceiveTouchGestureRecognizerDelegate
: UIGestureRecognizerDelegate
{
public override bool ShouldReceiveTouch (
UIGestureRecognizer recognizer,
UITouch touch)
{
return true;
}
}
#endregion
#region Helper Methods
protected void RegisterPanGesture()
{
UserInteractionEnabled = true;
UIPanGestureRecognizer pan = new UIPanGestureRecognizer();
pan.AddTarget(this, PanSelector);
pan.Delegate = new ReceiveTouchGestureRecognizerDelegate();
AddGestureRecognizer(pan);
}
#endregion
}
To use this control it's simply a matter of assigning images (Image and Slider properties) and adding the class as a sub-view:
UISlideToActivateImageView slideToActivate =
new UISlideToActivateImageView(new PointF(31, 214),
UIImage.FromFile("slidetoactivate.png"), UIImage.FromFile("slider.png"));
slideToActivate.Activate += delegate(object sender, EventArgs e) {
UIAlertView alert = new UIAlertView("Congratulations!",
"You've engaged the UISlideToActivateImageView!", null, "Okay");
alert.Show();
};
View.AddSubview(slideToActivate);
Pushing and popping controllers in a standard UINavigationController is a frequent activity when navigating an iOS app. The transition from controller to controller on the navigation stack may be optionally animated. Unfortunately, there's little choice regarding the animation itself. The developer determines whether or not the default right-to-left (push) or left-to-right (pop) effect is rendered by setting a boolean.
In some areas of my UI I wanted more control over the animations in my navigation stack. This thread on Stack Overflow was a great place to start. I ported some of the Objective-C code I found to C# extension methods as follows:
//Allows a UINavigationController to push using a custom animation transition
public static void PushControllerWithTransition(this UINavigationController
target, UIViewController controllerToPush,
UIViewAnimationOptions transition)
{
UIView.Transition(target.View, 0.75d, transition, delegate() {
target.PushViewController(controllerToPush, false);
}, null);
}
//Allows a UINavigationController to pop a using a custom animation
public static void PopControllerWithTransition(this UINavigationController
target, UIViewAnimationOptions transition)
{
UIView.Transition(target.View, 0.75d, transition, delegate() {
target.PopViewControllerAnimated(false);
}, null);
}
With these extensions in scope, moving between controllers with a flip animation is now as trivial as this:
//Pushing someController to the top of the stack
NavigationController.PushControllerWithTransition(someController,
UIViewAnimationOptions.TransitionFlipFromLeft);
//Popping the current controller off the top of the stack
NavigationController.PopControllerWithTransition(
UIViewAnimationOptions.TransitionFlipFromRight);
iOS 4 introduced local notifications, allowing apps to communicate brief text messages to users. In particular, a scheduled local notification can reach a user whether the app is running in the foreground, in the background or not running at all. While not as versatile as push notifications, scheduled local notifications can be helpful when an app needs to set-up predetermined alarms or reminders. Each app can have a total of 64 simultaneous scheduled local notifications (use them wisely so as to not annoy your users).
Below is an example of how to schedule a UILocalNotification using MonoTouch:
//Schedule one minute from the time of execution with no repeat
UILocalNotification notification = new UILocalNotification{
FireDate = DateTime.Now.AddMinutes(1),
TimeZone = NSTimeZone.LocalTimeZone,
AlertBody = "This is your scheduled local notification!",
RepeatInterval = 0
};
UIApplication.SharedApplication.ScheduleLocalNotification(notification);
You might notice that scheduled local notifications are automatically displayed when the current date/time surpasses our FireDate and the app is in the background or not running at all. However, when the app is in the foreground local notifications are seemingly ignored. This is by design. If the app is in the foreground you are in charge of responding to scheduled local notifications by overriding the ReceivedLocalNotification method as follows:
public override void ReceivedLocalNotification(UIApplication application,
UILocalNotification notification)
{
//Do something to respond to the scheduled local notification
UIAlertView alert = new UIAlertView("Notification Test",
notification.AlertBody, null, "Okay");
alert.Show();
}
When using SOAP-based web services (in this case with MonoDevelop / MonoTouch) I found I had to rely on the asynchronous proxy web methods to keep my UI responsive. Pretty common, right? Unfortunately, the asynchronous web methods seem to ignore the TimeOut property on the service. Without a timeout my users could be left waiting indefinitely on an unresponsive server. Not acceptable.
To get around this issue I came up with a way to cancel an asynchronous web method request after a set period of time using a Timer object. Maybe it can help others in a similar predicament? Here's some sample code:
protected void GetServiceData() { //Indicates that network activity is going on UIApplication.SharedApplication.NetworkActivityIndicatorVisible = true;
//Make the async call using (MyService service = new MyService()) { //Timer is set to go off one time after 15 seconds Timer serviceTimer = new Timer(15000); serviceTimer.AutoReset = false; serviceTimer.Elapsed += delegate(object source, ElapsedEventArgs e) { service.Abort(); throw new WebException("Timeout expired!"); }; serviceTimer.Enabled = true;
//Call the desired web method service.WebMethodCompleted += ServiceWebMethodCompleted; service.WebMethodAsync(serviceTimer); } }
//The async callback method protected void ServiceWebMethodCompleted(object sender, WebMethodCompletedEventArgs e) { using (NSAutoreleasePool pool = new NSAutoreleasePool()) { //Disable the timer that would abort this call with an exception //if the call to this web method took too long Timer serviceTimer = e.UserState as Timer; if (serviceTimer != null) { serviceTimer.Enabled = false; serviceTimer.Dispose(); }
if (e.Error != null) { if (e.Error is WebException) { //An error due to a timeout happened - handle it here } else { //Handle all other errors here } } else { //Async call successful - do something cool with e.Result }