I currently want to build a DatePicker with custom buttons on iOS using Xamarin. Out of the box there doesn't seem to be way to do.
Would anyone know how to build a custom renderer that would allow me to add a Cancel button and Next Button when the DatePickerDialog comes into focus?
I basically want this but at the top of this a Cancel button on the left and a next button on the right.
I was able to piece together a solution that does exactly what I want. Unfortunately, I had to use objectveC techniques mixed in with c# in order to get it to work. There is currently a issue triaged in the Xamarin project.
https://github.com/xamarin/Xamarin.Forms/issues/14156
For now here is my solution, which I hope helps others with their similar problems
using System;
using System.ComponentModel;
using System.Globalization;
using dcbel.Mobile.Controls;
using dcbel.Mobile.iOS.Extensions;
using dcbel.Mobile.iOS.Renderers;
using Foundation;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(CustomDatePicker), typeof(CustomDatePickerRenderer))]
namespace dcbel.Mobile.iOS.Renderers
{
/// <summary>
/// An extended date picker renderer.
/// </summary>
///
/// <seealso cref="T:Xamarin.Forms.Platform.iOS.DatePickerRenderer"/>
public class CustomDatePickerRenderer : DatePickerRenderer
{
/// <summary>
/// Executes the element property changed action.
/// </summary>
///
/// <param name="sender"> Source of the event. </param>
/// <param name="e"> A PropertyChangedEventArgs? to process. </param>
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs? e)
{
base.OnElementPropertyChanged(sender, e);
if (e != null && e.PropertyName == "DateTime" && this.Element is CustomDatePicker datePicker)
{
this.Control.Text = datePicker.DateTime.ToString(datePicker.Format, CultureInfo.CurrentCulture);
}
}
/// <summary>
/// Executes the element changed action.
/// </summary>
///
/// <param name="e"> An ElementChangedEventArgs{DatePicker} to process. </param>
protected override void OnElementChanged(ElementChangedEventArgs<DatePicker> e)
{
base.OnElementChanged(e);
if (this.Control != null && e?.NewElement is CustomDatePicker datePicker)
{
this.Control.BorderStyle = UITextBorderStyle.None;
if (this.Control.InputAccessoryView is UIToolbar toolbar)
{
var buttonTextAttributes = new UITextAttributes()
{
Font = UIFont.FromName("SFStrong", 15),
TextColor = UIColorExtensions.FromHex("5A5E62"),
};
using var doneButton = new UIBarButtonItem(datePicker.DoneButtonText, UIBarButtonItemStyle.Done, this, ObjCRuntime.Selector.FromHandle(ObjCRuntime.Selector.GetHandle("DoneButtonAction:")));
using var cancelButton = new UIBarButtonItem(datePicker.CancelButtonText, UIBarButtonItemStyle.Done, this, ObjCRuntime.Selector.FromHandle(ObjCRuntime.Selector.GetHandle("CancelButtonAction:")));
using var title = new UIBarButtonItem(datePicker.TitleText, UIBarButtonItemStyle.Plain, null) { Enabled = false };
using var space = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace);
doneButton.SetTitleTextAttributes(buttonTextAttributes, UIControlState.Normal);
title.SetTitleTextAttributes(buttonTextAttributes, UIControlState.Normal);
title.SetTitleTextAttributes(buttonTextAttributes, UIControlState.Disabled);
cancelButton.SetTitleTextAttributes(buttonTextAttributes, UIControlState.Normal);
toolbar.SetItems(new UIBarButtonItem[] { cancelButton, space, title, space, doneButton }, true);
}
if (this.Control.InputView is UIDatePicker uiDatePicker)
{
uiDatePicker.Mode = UIDatePickerMode.DateAndTime;
}
}
}
[Action("CancelButtonAction:")]
private void CancelButtonAction(NSObject sender)
{
this.Control.ResignFirstResponder();
this.Element.Unfocus();
}
[Action("DoneButtonAction:")]
private void DoneButtonAction(NSObject sender)
{
var barButton = (UIBarButtonItem)sender;
if (barButton.Target is CustomDatePickerRenderer datePickerRenderer)
{
if (datePickerRenderer.Control?.InputView is UIDatePicker uiDatePicker)
{
var dateSelected = uiDatePicker.Date.ToDateTime();
((CustomDatePicker)this.Element).DateTime = dateSelected;
}
}
this.Control.ResignFirstResponder();
this.Element.Unfocus();
}
}
}
Related
My Team-recording bot code is running in azure Kubernetes service. The code is written in the .netframework language. The bot can join the team meeting and it is recording the meeting and saving the files in audio(wav) format to local in a .zip file. I want to process the video files to local in a .zip file. Right now we have AudioProcessor class to get the audio files:` public BotMediaStream(
ILocalMediaSession mediaSession,
string callId,
IGraphLogger logger,
IEventPublisher eventPublisher,
IAzureSettings settings
)
: base(logger)
{
ArgumentVerifier.ThrowOnNullArgument(mediaSession, nameof(mediaSession));
ArgumentVerifier.ThrowOnNullArgument(logger, nameof(logger));
ArgumentVerifier.ThrowOnNullArgument(settings, nameof(settings));
this.participants = new List<IParticipant>();
_eventPublisher = eventPublisher;
_callId = callId;
_mediaStream = new MediaStream(
settings,
logger,
mediaSession.MediaSessionId.ToString()
);
// Subscribe to the audio media.
this._audioSocket = mediaSession.AudioSocket;
if (this._audioSocket == null)
{
throw new InvalidOperationException("A mediaSession needs to have at least an audioSocket");
}
this._audioSocket.AudioMediaReceived += this.OnAudioMediaReceived;
this.videoSockets = mediaSession.VideoSockets?.ToList();
if (this.videoSockets?.Any() == true)
{
this.videoSockets.ForEach(videoSocket => videoSocket.VideoMediaReceived += this.OnVideoMediaReceived);
}
// Subscribe to the VBSS media.
this.vbssSocket = mediaSession.VbssSocket;
if (this.vbssSocket != null)
{
mediaSession.VbssSocket.VideoMediaReceived += this.OnVbssMediaReceived;
}
}
`
private ILocalMediaSession CreateLocalMediaSession(Guid mediaSessionId = default)
{
try
{
// create media session object, this is needed to establish call connections
var videoSocketSettings = new List<VideoSocketSettings>();
// create the receive only sockets settings for the multiview support
for (int i = 0; i < SampleConstants.NumberOfMultiviewSockets; i++)
{
videoSocketSettings.Add(new VideoSocketSettings
{
StreamDirections = StreamDirection.Recvonly,
ReceiveColorFormat = VideoColorFormat.H264,
});
}
// Create the VBSS socket settings
var vbssSocketSettings = new VideoSocketSettings
{
StreamDirections = StreamDirection.Recvonly,
ReceiveColorFormat = VideoColorFormat.H264,
MediaType = MediaType.Vbss,
SupportedSendVideoFormats = new List<VideoFormat>
{
// fps 1.875 is required for h264 in vbss scenario.
VideoFormat.H264_1920x1080_1_875Fps,
},
};
// create media session object, this is needed to establish call connections
var mediaSession = this.Client.CreateMediaSession(
new AudioSocketSettings
{
StreamDirections = StreamDirection.Recvonly,
SupportedAudioFormat = AudioFormat.Pcm16K,
},
videoSocketSettings,
vbssSocketSettings,
mediaSessionId: mediaSessionId);
return mediaSession;
}
catch (Exception e)
{
_logger.Log(System.Diagnostics.TraceLevel.Error, e.Message);
throw;
}
}
Here audio is getting processed from team-recording-bot
AudioProcessor code: `// ***********************************************************************
// Assembly : RecordingBot.Services
// Author : JasonTheDeveloper
// Created : 09-07-2020
//
// Last Modified By : dannygar
// Last Modified On : 09-07-2020
// ***********************************************************************
// <copyright file="AudioProcessor.cs" company="Microsoft">
// Copyright © 2020
// </copyright>
// <summary></summary>
// ***********************************************************************
using NAudio.Wave;
using RecordingBot.Model.Constants;
using RecordingBot.Services.Contract;
using RecordingBot.Services.ServiceSetup;
using RecordingBot.Services.Util;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
namespace RecordingBot.Services.Media
{
/// <summary>
/// Class AudioProcessor.
/// Implements the <see cref="RecordingBot.Services.Util.BufferBase{RecordingBot.Services.Media.SerializableAudioMediaBuffer}" />
/// </summary>
/// <seealso cref="RecordingBot.Services.Util.BufferBase{RecordingBot.Services.Media.SerializableAudioMediaBuffer}" />
public class AudioProcessor : BufferBase<SerializableAudioMediaBuffer>
{
/// <summary>
/// The writers
/// </summary>
readonly Dictionary<string, WaveFileWriter> _writers = new Dictionary<string, WaveFileWriter>();
/// <summary>
/// The processor identifier
/// </summary>
private readonly string _processorId = null;
/// <summary>
/// The settings
/// </summary>
private readonly AzureSettings _settings;
/// <summary>
/// Initializes a new instance of the <see cref="AudioProcessor" /> class.
/// </summary>
/// <param name="settings">The settings.</param>
public AudioProcessor(IAzureSettings settings)
{
_processorId = Guid.NewGuid().ToString();
_settings = (AzureSettings)settings;
}
/// <summary>
/// Processes the specified data.
/// </summary>
/// <param name="data">The data.</param>
protected override async Task Process(SerializableAudioMediaBuffer data)
{
if (data.Timestamp == 0)
{
return;
}
var path = Path.Combine(Path.GetTempPath(), BotConstants.DefaultOutputFolder, _settings.MediaFolder, _processorId);
// First, write all audio buffer, unless the data.IsSilence is checked for true, into the all speakers buffer
var all = "all";
var all_writer = _writers.ContainsKey(all) ? _writers[all] : InitialiseWavFileWriter(path, all);
if (data.Buffer != null)
{
// Buffers are saved to disk even when there is silence.
// If you do not want this to happen, check if data.IsSilence == true.
await all_writer.WriteAsync(data.Buffer, 0, data.Buffer.Length).ConfigureAwait(false);
}
if (data.SerializableUnmixedAudioBuffers != null)
{
foreach (var s in data.SerializableUnmixedAudioBuffers)
{
if (string.IsNullOrWhiteSpace(s.AdId) || string.IsNullOrWhiteSpace(s.DisplayName))
{
continue;
}
var id = s.AdId;
var writer = _writers.ContainsKey(id) ? _writers[id] : InitialiseWavFileWriter(path, id);
// Write audio buffer into the WAV file for individual speaker
await writer.WriteAsync(s.Buffer, 0, s.Buffer.Length).ConfigureAwait(false);
// Write audio buffer into the WAV file for all speakers
await all_writer.WriteAsync(s.Buffer, 0, s.Buffer.Length).ConfigureAwait(false);
}
}
}
/// <summary>
/// Initialises the wav file writer.
/// </summary>
/// <param name="rootFolder">The root folder.</param>
/// <param name="id">The identifier.</param>
/// <returns>WavFileWriter.</returns>
private WaveFileWriter InitialiseWavFileWriter(string rootFolder, string id)
{
var path = AudioFileUtils.CreateFilePath(rootFolder, $"{id}.wav");
// Initialize the Wave Format using the default PCM 16bit 16K supported by Teams audio settings
var writer = new WaveFileWriter(path, new WaveFormat(
rate: AudioConstants.DefaultSampleRate,
bits: AudioConstants.DefaultBits,
channels: AudioConstants.DefaultChannels));
_writers.Add(id, writer);
return writer;
}
/// <summary>
/// Finalises the wav writing and returns a list of all the files created
/// </summary>
/// <returns>System.String.</returns>
public async Task<string> Finalise()
{
//drain the un-processed buffers on this object
while (Buffer.Count > 0)
{
await Task.Delay(200);
}
var archiveFile = Path.Combine(Path.GetTempPath(), BotConstants.DefaultOutputFolder, _settings.MediaFolder, _processorId, $"{Guid.NewGuid()}.zip");
try
{
using (var stream = File.OpenWrite(archiveFile))
{
using (ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create))
{
// drain all the writers
foreach (var writer in _writers.Values)
{
var localFiles = new List<string>();
var localArchive = archive; //protect the closure below
var localFileName = writer.Filename;
localFiles.Add(writer.Filename);
await writer.FlushAsync();
writer.Dispose();
// Is Resampling and/or mono to stereo conversion required?
if (_settings.AudioSettings.WavSettings != null)
{
// The resampling is required
localFiles.Add(AudioFileUtils.ResampleAudio(localFileName, _settings.AudioSettings.WavSettings, _settings.IsStereo));
}
else if (_settings.IsStereo) // Is Stereo audio required?
{
// Convert mono WAV to stereo
localFiles.Add(AudioFileUtils.ConvertToStereo(localFileName));
}
// Remove temporary saved local WAV file from the disk
foreach (var localFile in localFiles)
{
await Task.Run(() =>
{
var fInfo = new FileInfo(localFile);
localArchive.CreateEntryFromFile(localFile, fInfo.Name, CompressionLevel.Optimal);
File.Delete(localFile);
}).ConfigureAwait(false);
}
}
}
}
}
finally
{
await End();
}
return archiveFile;
}
}
}
`
I request you to please help us on this. We need to get video from team-recording-bot and save it to local in.zip file and also to s3 bucket. Please, Please help us on this.
There doesn't appear to be a lot of people using Xamarin for Visual Studio consequently there isn't a lot of information specific to that platform out there.
Having said that, I've been trying to get a Floating Action Button (FAB) to work and it's been quite the exercise. I finally got it to appear and assign it to a variable in the activity with help from the nice folks who use StackOverflow, but cannot get the android:onClick="FabOnClick" call to work. Clicking on the FAB causes the app to crash with the error:
Unhandled Exception:
Java.Lang.IllegalStateException: Could not find method FabOnClick(View) in a parent or ancestor Context for android:onClick attribute defined on view class android.support.design.widget.FloatingActionButton with id 'fab' occurred
This is the code in my activity:
public void FabOnClick(View v)
{
int x = 1;
}
It doesn't really do anything because I'm just trying to capture the click event for now. I set a breakpoint on the int x = 1 line to see when it's is executed. So what am I missing?
* Update *
I updated my activity code based on #Digitalsa1nt's answer below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Acr.UserDialogs;
using Android.Net;
using System.Net;
using Android.Support.Design.Widget;
using System.Threading.Tasks;
using Android.Views.InputMethods;
using static Android.Views.View;
namespace OML_Android
{
[Activity(Label = "CreateAccount")]
public class CreateAccount : Activity
{
public string result = "";
public EditText aTextboxUsername;
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.CreateAccount);
RequestedOrientation = Android.Content.PM.ScreenOrientation.Portrait;
aTextboxUsername = FindViewById<EditText>(Resource.Id.aTextboxUsername);
EditText aTextboxPassword = FindViewById<EditText>(Resource.Id.aTextboxPassword);
EditText aTextboxPassword2 = FindViewById<EditText>(Resource.Id.aTextboxPassword2);
EditText txtEmailAddress = FindViewById<EditText>(Resource.Id.txtEmailAddress);
EditText txtEmailAddress2 = FindViewById<EditText>(Resource.Id.txtEmailAddress2);
EditText txtFirstName = FindViewById<EditText>(Resource.Id.first_name);
EditText txtMI = FindViewById<EditText>(Resource.Id.mi);
EditText txtLastName = FindViewById<EditText>(Resource.Id.last_name);
EditText txtAddress = FindViewById<EditText>(Resource.Id.address);
EditText txtCity = FindViewById<EditText>(Resource.Id.city);
Spinner spnState = FindViewById<Spinner>(Resource.Id.state);
EditText txtZip = FindViewById<EditText>(Resource.Id.zip);
MaskedEditText.MaskedEditText txtPhone = FindViewById<MaskedEditText.MaskedEditText>(Resource.Id.phone);
Spinner spnCompany = FindViewById<Spinner>(Resource.Id.company_spinner);
Spinner spnDept = FindViewById<Spinner>(Resource.Id.department_spinner);
Spinner spnSection = FindViewById<Spinner>(Resource.Id.section_spinner);
Button ButtonSubmit = FindViewById<Button>(Resource.Id.button_submit);
ScrollView sv = FindViewById<ScrollView>(Resource.Id.scrollView1);
ButtonSubmit.SetBackgroundColor(Android.Graphics.Color.YellowGreen);
// Hide the keyboard (also doesn't work)
InputMethodManager board = (InputMethodManager)GetSystemService(Context.InputMethodService);
board.HideSoftInputFromWindow(aTextboxUsername.WindowToken, 0);
// get the floating action button.
FloatingActionButton myFab = FindViewById< FloatingActionButton>(Resource.Id.fab);
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
DataInterfaceWeb.DataInterface myService = new DataInterfaceWeb.DataInterface();
myFab.Click += FabButton_Click(); // <-- get error here
try
{
ConnectivityManager connectivityManager = (ConnectivityManager)GetSystemService(ConnectivityService);
NetworkInfo activeConnection = connectivityManager.ActiveNetworkInfo;
bool isOnline = (activeConnection != null) && activeConnection.IsConnected;
if (!isOnline)
{
showMessage("There is no internet or cell phone connection. Connect to a network or connect to a cellular network.", "ERROR");
}
}
catch (Exception ex)
{
showMessage("Connectivity Manager failed to create a connection due to error: " + ex.Message, "ERROR");
};
// Create your application here
ButtonSubmit.Click += async (sender, e) =>
{
try
{
result = myService.CheckForUser(Master.username, Master.password, aTextboxUsername.Text);
if (result.ToUpper() == "Y")
{
await showMessage("Username " + aTextboxUsername.Text + " is already in use. Please choose another", "ERROR");
// aTextboxUsername.SetSelectAllOnFocus(true);
aTextboxUsername.RequestFocus();
View insideView = FindViewById<EditText>(Resource.Id.aTextboxUsername);
sv.ScrollTo(0, (int)insideView.GetY());
aTextboxUsername.SelectAll();
}
}
catch (Exception ex)
{
showMessage("Account creation attempt failed due to error: " + ex.Message, "ERROR");
}
};
}
public async Task showMessage(string message, string messageType)
{
var result = await UserDialogs.Instance.ConfirmAsync(new ConfirmConfig
{
Message = messageType + System.Environment.NewLine + message,
OkText = "Ok",
});
}
public void FabButton_Click()
{
int x = 1;
}
}
}
The error I get now is:
Cannot implicitly convert 'void' to 'SystemEventHandler' on the line myFab.Click += FabButton_Click();.
#Digitalsa1nt did point me in the right direction. Instead of
fabButton.Click += FabButton_Click;
I just wired up an event, as the error suggested (duh):
myFab.Click += (sender, e) =>
{
FabButton_Click();
};
It now works as I would expect.
So I'm making a couple of assumptions in this answer. Firstly that you are working with a Xamarin.Native project and not a Xamarin.Forms project.
Secondly I am assuming you are using the FloatingActionButton from one of the support libraries such as: Android.Support.Design.Widget (base / V4 / V7).
Once you've defined your FAB within the AXML Layout page:
<android.support.design.widget.FloatingActionButton
app:backgroundTint="#color/colourPrimary"
android:id="#+id/fabButton"
android:src="#drawable/image"
app:fabSize="normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:elevation="16dp"
android:translationZ="12dp"
app:rippleColor="#ffa9a9a9" />
You can get it from within your activity as such:
using Android.Support.Design.Widget;
// declare variable
private FloatingActionButton fabButton;
public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// call base
base.OnCreateView(inflater, container, savedInstanceState);
// inflate our view
var view = inflater.Inflate(Resource.Layout.MainTabWishlistPage, container, false);
// get our instance of the button using the resource ID.
fabButton = view.FindViewById<FloatingActionButton>(Resource.Id.fabButton);
// assign to click event
fabButton.Click += FabButton_Click;
}
private void FabButton_Click(object sender, EventArgs e)
{
int x = 1;
}
The above example is based on it being a fragment rather than an activity, but the methodology is the same.
Official Git Repo:
Xamarin/monodroid-samples - Floating Action Button Basic
Random online guide:
android-material-design-floating-action
In case this is a Xamarin.Forms project, look into James Montemagno's library (p.s one of the developers that works on Xamarin and creates tons of libraries to help make your life easier, definitely look through his other repos.)
jamesmontemagno/FloatingActionButton-for-Xamarin.Android
I have created a tutorial window in storyboard with two views, one to hold show the tutorial the other used as a template for each page of content.
Some elements are coded programmatically on the ViewDidLoad event.
The PageViewController is working 100% as required, it shows the three pages of content and allows swiping backwards and forwards without issue.
I've added a UIPageControl programmatically to the main ViewController but for the life of me cannot update its CurrentPage value correctly. Accessing the datasources PageIndex value gives me odd results when swiping back and forth.
Is there a reliable way to know exactly which page is been displayed ?
Or to know which direction the page transition moved, this way I can manually update? Not entirely sure how UIPageViewControllerNavigationDirection is accessed from the pagecontrollers 'DidFinishAnimating' event.
My main view controller code is as follows:
using Foundation;
using System;
using UIKit;
namespace Performance
{
partial class VCOnboardHome : UIViewController
{
const int pageCount = 3;
public UIPageViewController pvcOnboarding{ get; set; }
private OnboardingDataSource onboardDataSource;
UIStoryboard board;
public UIPageControl pgControlIndicator;
public VCOnboardHome (IntPtr handle) : base (handle)
{
}
/// <summary>
/// ViewDidLoad event method
/// </summary>
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
board = UIStoryboard.FromName ("Main", null);
// Programmatically create a PageView controller
pvcOnboarding = new UIPageViewController (UIPageViewControllerTransitionStyle.Scroll,
UIPageViewControllerNavigationOrientation.Horizontal,
UIPageViewControllerSpineLocation.None, 20f);
// PageView Controller datasource
var views = CreateViews ();
onboardDataSource = new OnboardingDataSource (views);
pvcOnboarding.DataSource = onboardDataSource;
pvcOnboarding.SetViewControllers (new UIViewController[] { views [0] },
UIPageViewControllerNavigationDirection.Forward,
false, null);
// Set PageView size
pvcOnboarding.View.Frame = View.Bounds;
// Add the page view control to this view controller
Add (pvcOnboarding.View);
// Create Page Control
var frame = UIScreen.MainScreen.Bounds;
pgControlIndicator = new UIPageControl ();
pgControlIndicator.Frame = new CoreGraphics.CGRect (20f, frame.Height - 60f, frame.Width - 40f, 40f);
pgControlIndicator.Pages = pageCount;
pgControlIndicator.UserInteractionEnabled = false;
Add (pgControlIndicator);
// Update the Page Control to indicate the current page we are showing.
// Only do this if the full page transition happened and not a partial page turn
pvcOnboarding.DidFinishAnimating += (sender, e) => {
foreach(UIViewController u in e.PreviousViewControllers){
// TODO - Not needed, remove once page control working
// u = the previous viewcontroller
}
if(e.Finished && e.Completed){
// Page transition completed
// Update Page Control here
}else{
// Incomplete page transition
}
};
}
// Content for each page
VCOnboardContentNew[] CreateViews ()
{
var pageData = new [] {
new ContentOnBoardData {
headerLblText = #"Page 1",
bodyContentText = #"Page 1 body text blah blah blah blah",
buttonText = #"Ok, next no. 1",
pageImage = UIImage.FromBundle("ios_images_v2/onboarding/icon-qr-code.png"),
currentPage = 1,
totalPages = 3
},
new ContentOnBoardData {
headerLblText = #"Page 2",
bodyContentText = #"Page 2 body text blah blah blah blah",
buttonText = #"Ok, next no. 2",
pageImage = UIImage.FromBundle("ios_images_v2/onboarding/icon-id-check.png"),
currentPage = 2,
totalPages = 3
},
new ContentOnBoardData {
headerLblText = #"Page 3",
bodyContentText = #"Page 3 body text blah blah blah blah",
buttonText = #"Ok, got it",
pageImage = null,
currentPage = 3,
totalPages = 3
}
};
var views = new VCOnboardContentNew[pageData.Length];
for (int i = 0; i < pageCount; i++) {
int pageIndex = i;
views [i] = (VCOnboardContentNew)board.InstantiateViewController ("sbid_onboardcontent");
views [i].PageIndex = pageIndex;
views [i].HeaderText = pageData [i].headerLblText;
views [i].ContentText = pageData [i].bodyContentText;
views [i].PageImage = pageData [i].pageImage;
views [i].CurrentPage = pageData [i].currentPage;
views [i].TotalPages = pageCount;
views [i].ButtonText = pageData [i].buttonText;
views [i].ButtonClicked += (s, e) => {
DismissViewController (true, null);
};
}
return views;
}
}
/// <summary>
/// Onboarding data source.
/// </summary>
class OnboardingDataSource : UIPageViewControllerDataSource
{
readonly VCOnboardContentNew[] _views;
public OnboardingDataSource (VCOnboardContentNew[] views)
{
_views = views;
}
/// <summary>
/// Gets the previous view controller.
/// </summary>
/// <returns>The previous view controller.</returns>
/// <param name="pageViewController">Page view controller.</param>
/// <param name="referenceViewController">Reference view controller.</param>
public override UIViewController GetPreviousViewController (UIPageViewController pageViewController, UIViewController referenceViewController)
{
int index = ((VCOnboardContentNew)referenceViewController).PageIndex;
bool controlCheck = (index <= 0);
UIViewController vcToReturn = controlCheck ? null : (_views [index - 1]);
return vcToReturn;
}
/// <summary>
/// Gets the next view controller.
/// </summary>
/// <returns>The next view controller.</returns>
/// <param name="pageViewController">Page view controller.</param>
/// <param name="referenceViewController">Reference view controller.</param>
public override UIViewController GetNextViewController (UIPageViewController pageViewController, UIViewController referenceViewController)
{
int index = ((VCOnboardContentNew)referenceViewController).PageIndex;
bool controlCheck = index + 1 >= _views.Length;
UIViewController vcToReturn = controlCheck ? null : _views [index + 1];
return vcToReturn;
}
}
/// <summary>
/// Content onboard data.
/// </summary>
struct ContentOnBoardData
{
public string headerLblText;
public string bodyContentText;
public string buttonText;
public UIImage pageImage;
public int currentPage;
public int totalPages;
}
}
My Page content code view controller is as follows:
using System;
using UIKit;
namespace Performance
{
/// <summary>
/// Class: VCOnboardContentNew
/// </summary>
partial class VCOnboardContentNew : UIViewController
{
public EventHandler ButtonClicked;
public int PageIndex { get; set; }
public string HeaderText{ get; set; }
public UIImage PageImage{ get; set; }
public int CurrentPage{ get; set; }
public int TotalPages{ get; set; }
public string ContentText{ get; set; }
public string ButtonText{ get; set; }
public VCOnboardContentNew (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
// Set text for the main title
lblTest.Font = UIFont.FromName("FreightDispLight", 26f);
lblTest.Text = HeaderText;
// Set text for body content
lblContentBodyText.Font = UIFont.FromName("FreightDispLight", 14f);
lblContentBodyText.Text = ContentText;
pgCtrlWhichPageWeOn.Hidden = true;
if(PageImage != null){
pageContentImage.Hidden = false;
pageContentImage.Image = PageImage;
}else{
pageContentImage.Hidden = true;
}
btnCallToAction.SetTitle (ButtonText, UIControlState.Normal);
btnCallToAction.TouchUpInside += (object sender, EventArgs e) => {
if (ButtonClicked != null) {
ButtonClicked.Invoke (this, null);
}
};
}
}
}
When the transition style is set to scroll, the page view controller seems to:
As soon as one transition finishes, it immediately calls for the next (or previous) View Controller (with GetNextViewController), even though the user might not have started the next swipe; and
It keeps the view controller that was transitioned from, and assumes that it is the previous VC, so it doesn't call for the previous VC (with GetPreviousViewController) if the user swipes back.
This makes it pretty difficult to keep track of which VC is actually currently showing pretty difficult. If found (see this answer) that I had to use both the WillTransition event and the DidFinishAnimating event. I'm not familiar with Xamarin and C#, so forgive me if this syntax is way off, but I think something like this:
pvcOnboarding.WillTransition += (sender, e) => {
nextVCIndex = ((VCOnboardContentNew)e.PendingViewControllers[0]).PageIndex
}
pvcOnboarding.DidFinishAnimating += (sender, e) => {
if(e.Finished && e.Completed){
// Page transition completed
currentVCIndex = nextVCIndex
// Update Page Control here
}else{
// Incomplete page transition
}
};
You'll need to add currentVCIndex and nextVCIndex as class level variables.
Old question but was recently struggling with the same issue. Finally found a nice solution.
Provide your UIPageViewController with:
public static int pageIndex = 0; // or whatever start index
Then for each of your UIViewControllers to be loaded override ViewDidAppear:
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
MyCystomPageViewController.pageIndex = 1; // the index of this viewcontroller
}
This works since ViewDidAppear gets called when the view is added as subview. And thus will be called everytime you swipe.
I read the following article and tried it, but it won't work:
How to make a uiactionsheet dismiss when you tap outside eg above it?
I have a Menu-Class (Superclass from uiactionsheet) and register a TapRecognizer in it. When the UIActionSheet is visible and a tap on it (not outside) occurd, it executes the Taprecognizer. But on a tap outside, nothing happend.
public class MyMenu: UIActionSheet
{
/// <summary>
/// Constructor
/// </summary>
public MyMenu(UIView owner)
{
_owner = owner;
}
public override void Show()
{
ShowInView(_owner);
var rec = new UITapGestureRecognizer(CheckTapOutside);
rec.CancelsTouchesInView = false;
this.Superview.AddGestureRecognizer(rec);
// Add controls to it
}
public void Hide()
{
DismissWithClickedButtonIndex(0, true);
}
private void CheckTapOutside(UITapGestureRecognizer rec)
{
var p = rec.LocationInView(this);
if (p.Y < 0)
{
this.Hide();
}
}
}
The following code initializes the menu and shows it.
var menu = new MyMenu(View);
menu.Show();
Can anyone tell me what's going wrong? Thanks.
The elegant Action Syntax in the MVCContrib Grid gives us the Empty() method. However, the default behavior of MvcContrib.UI.Grid.GridRenderer<T>.RenderHeader() is to hide the table column headers when the grid is empty. Is there a way to show headers when data is not present that does not require a major refactoring?
Now I have heard of hiding the headers by default and hard-coding something but this is not cool to me.
By the way, this is what is happening under the hood (in MvcContrib.UI.Grid.GridRenderer<T>):
protected virtual bool RenderHeader()
{
//No items - do not render a header.
if(! ShouldRenderHeader()) return false;
RenderHeadStart();
foreach(var column in VisibleColumns())
{
//Allow for custom header overrides.
if(column.CustomHeaderRenderer != null)
{
column.CustomHeaderRenderer(new RenderingContext(Writer, Context, _engines));
}
else
{
RenderHeaderCellStart(column);
RenderHeaderText(column);
RenderHeaderCellEnd();
}
}
RenderHeadEnd();
return true;
}
protected virtual bool ShouldRenderHeader()
{
return !IsDataSourceEmpty();
}
protected bool IsDataSourceEmpty()
{
return DataSource == null || !DataSource.Any();
}
You can override the ShouldRenderHeader() method of the HtmlTableGridRenderer class.
public class AlwaysRenderHeaderRenderer<T>
: HtmlTableGridRenderer<T> where T : class
{
protected override bool ShouldRenderHeader()
{
return true;
}
}
<%= Html.Grid(Model).RenderUsing(new AlwaysRenderHeaderRenderer<TypeOfModel>()) %>
A side effect of this approach is that when the grid is empty, an empty table body will be rendered instead of a message. Any text provided to Empty() is ignored. This wasn't a problem for me since I'm manipulating the table on the client side with JavaScript anyway, but be warned.
When inheriting from HtmlTableGridRenderer you can also override RenderEmpty to eliminate the problem Brant ran into.
protected override void RenderEmpty()
{
RenderHeadStart();
foreach(var column in VisibleColumns())
{
RenderHeaderCellStart(column);
RenderHeaderText(column);
RenderHeaderCellEnd();
}
RenderHeadEnd();
RenderBodyStart();
RenderText("<tr><td colspan=\"" + VisibleColumns().Count() + "\">" + GridModel.EmptyText + "</td></tr>");
RenderBodyEnd();
}
In your custom Grid Renderer (subclass GridRenderer<T>) use overrides like these:
/// <summary>
/// Renders the <c>table</c> header.
/// </summary>
/// <returns>
/// Returns the negative of the results
/// of <see cref="GridRenderer<T>.IsDataSourceEmpty"/>.
/// </returns>
/// <remarks>
/// The return value of <see cref="GridRenderer<T>.IsDataSourceEmpty"/>
/// in this override has no effect on whether the Grid header is rendered.
///
/// However, this return value is used
/// by <see cref="GridRenderer<T>.Render"/>
/// to run <see cref="GridRenderer<T>.RenderItems"/>
/// or <see cref="GridRenderer<T>.RenderEmpty"/>.
/// </remarks>
protected override bool RenderHeader()
{
RenderHeadStart();
foreach(var column in VisibleColumns())
{
//Allow for custom header overrides.
if(column.CustomHeaderRenderer != null)
{
column.CustomHeaderRenderer(new RenderingContext(Writer, Context, _engines));
}
else
{
RenderHeaderCellStart(column);
RenderHeaderText(column);
RenderHeaderCellEnd();
}
}
RenderHeadEnd();
return !base.IsDataSourceEmpty();
}
…
protected override void RenderEmpty()
{
RenderBodyStart();
WriteNoRecordsAvailable(base.Writer, this._numberOfTableColumns);
RenderBodyEnd();
}
Note that WriteNoRecordsAvailable() is my custom method so it can be ignored.
Finally:
/// <summary>
/// This private member is duplicated
/// in order to override <see cref="GridRenderer<T>.RenderHeader"/>.
/// </summary>
readonly ViewEngineCollection _engines;
…
/// <summary>
/// Initializes a new instance of the <see cref="CrmHtmlTableGridRenderer<T>"/> class.
/// </summary>
/// <param name="engines">The engines.</param>
public CrmHtmlTableGridRenderer(ViewEngineCollection engines)
: base(engines)
{
_engines = engines;
}
Can't you just comment out the check to see if it should render the header. Am I missing something or do you want it to always display the header?
If so then comment out that line.
//if(! ShouldRenderHeader()) return false;
I haven't looks at all the code but from your code snippet it seems like that should work.
A combination of David's and Brant's answers:
protected override bool ShouldRenderHeader()
{
return true;
}
// Oddly Render relies on ShouldRenderHeader to return IsDataSourceEmpty
// so RenderItems will always be called.
protected override void RenderItems()
{
if (IsDataSourceEmpty())
RenderEmpty();
else
base.RenderItems();
}
protected override void RenderEmpty()
{
RenderBodyStart();
RenderText("<tr><td colspan=\"" + VisibleColumns().Count() + "\">" + GridModel.EmptyText + "</td></tr>");
RenderBodyEnd();
}