I use Xamarin.Forms, but I do the necessary actions on the iOS side, but Facebook does not verify the update.
and I'm new to this problem I had no such problems in advance.
I using plugin: Xamarin.Facebook.iOS (4.39.1)
SDK is up to date, although I have done all the necessary applications, my problem is not solved.
using System;
using System.Linq;
using Firebase.Analytics;
using Foundation;
using Google.TagManager;
using Plugin.FirebasePushNotification;
using UIKit;
namespace XXX.iOS
{
[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
public bool disableAllOrientation = true;
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
Facebook.CoreKit.Settings.AppID = "xxx";
Facebook.CoreKit.Settings.DisplayName = "xxxxx";
Rg.Plugins.Popup.Popup.Init();
global::Xamarin.Forms.Forms.Init();
LoadApplication(new App());
TagManager.Configure();
Firebase.Core.App.Configure();
GoogleConversionTracking.Unified.ACTConversionReporter.ReportWithConversionId("xxxx", "xxxx", "0.00", false);
Facebook.CoreKit.ApplicationDelegate.SharedInstance.FinishedLaunching(app, options);
FirebasePushNotificationManager.Initialize(options, true);
CrossFirebasePushNotification.Current.RegisterForPushNotifications();
return base.FinishedLaunching(app, options);
}
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
{
if (disableAllOrientation == true)
{
return UIInterfaceOrientationMask.Portrait;
}
return UIInterfaceOrientationMask.All;
}
public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
FirebasePushNotificationManager.DidRegisterRemoteNotifications(deviceToken);
}
public override void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
{
FirebasePushNotificationManager.RemoteNotificationRegistrationFailed(error);
}
public override void DidReceiveRemoteNotification(UIApplication application, NSDictionary userInfo, Action<UIBackgroundFetchResult> completionHandler)
{
FirebasePushNotificationManager.DidReceiveMessage(userInfo);
System.Console.WriteLine(userInfo);
}
public override void OnActivated(UIApplication uiApplication)
{
FirebasePushNotificationManager.Connect();
// FacebookClientManager.OnActivated();
Facebook.CoreKit.AppEvents.ActivateApp();
}
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
return Facebook.CoreKit.ApplicationDelegate.SharedInstance.OpenUrl(app, url, options);
// return FacebookClientManager.OpenUrl(app, url, options);
}
public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation)
{
Analytics.HandleOpenUrl(url);
return
Facebook.CoreKit.ApplicationDelegate.SharedInstance.OpenUrl(application, url, sourceApplication,annotation);
///eturn FacebookClientManager.OpenUrl(application, url, sourceApplication, annotation);
}
public override void DidEnterBackground(UIApplication application)
{
// Use this method to release shared resources, save user data, invalidate timers and store the application state.
// If your application supports background exection this method is called instead of WillTerminate when the user quits.
var lastObject = UIApplication.SharedApplication.KeyWindow.Subviews.Last();
if (lastObject != null)
{
UIVisualEffect blurEffect = UIBlurEffect.FromStyle(UIBlurEffectStyle.Dark);
UIVisualEffectView visualEffectView = new UIVisualEffectView(blurEffect)
{
Frame = UIScreen.MainScreen.Bounds
};
lastObject.AddSubview(visualEffectView);
}
base.DidEnterBackground(application);
FirebasePushNotificationManager.Disconnect();
}
public override void WillEnterForeground(UIApplication uiApplication)
{
var lastObject = UIApplication.SharedApplication.KeyWindow.Subviews.Last();
if (lastObject != null)
{
var lastSub = lastObject.Subviews.Last();
lastSub.RemoveFromSuperview();
}
base.WillEnterForeground(uiApplication);
}
public override void WillTerminate(UIApplication uiApplication)
{
UIPasteboard.General.Items = new NSDictionary[0];
base.WillTerminate(uiApplication);
}
}
}
Related
I'm new with Xamarin.Forms and I have implemented a custom web view renderer in Droid project.
The issue is when implementing the renderer in iOS project, it's like the Webview is initialized without loading the CSS and Javascript. Because it only display HTML page without any functionality.
After some research, I know we have to implement WKWebviewRenderer and we have to load the LoadFileUrl method in iOS but I still can't catch the url in the renderer.
Anyone have an idea please about how to implement the following Android Renderer code into iOS Renderer??
Custom renderer in Droid project:
[assembly: ExportRenderer(typeof(Xamarin.Forms.WebView), typeof(MyProject.Droid.WebViewRenderer))]
namespace MyProject.Droid
{
public class WebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
private bool isMyCustomWebview = false;
public IWebViewController ElementController => Element;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (e.NewElement.GetType() == typeof(MyCustomWebview))
{
Control.SetWebViewClient(new Callback(this));
isMyCustomWebview = true;
}
else
{
Control.SetWebViewClient(new Callback(Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity));
isMyCustomWebview = false;
}
}
public class Callback : WebViewClient
{
Activity _context;
public Callback(Activity _context)
{
this._context = _context;
}
WebViewRenderer _renderer;
public Callback(WebViewRenderer renderer)
{
_renderer = renderer ?? throw new ArgumentNullException("Renderer");
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
if (_renderer != null && _renderer.isMyCustomWebview)
{
DependencyService.Get<ILoadingIndicator>().Show();
var args = new WebNavigatingEventArgs(WebNavigationEvent.NewPage, new UrlWebViewSource { Url = url }, url);
_renderer.ElementController.SendNavigating(args);
}
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
if (_renderer != null && _renderer.isMyCustomWebview)
{
DependencyService.Get<ILoadingIndicator>().Dismiss();
var source = new UrlWebViewSource { Url = url };
var args = new WebNavigatedEventArgs(WebNavigationEvent.NewPage, source, url, WebNavigationResult.Success);
_renderer.ElementController.SendNavigated(args);
}
}
}
}
}
UPDATE: Custom renderer in iOS project:
[assembly: ExportRenderer(typeof(Xamarin.Forms.WebView), typeof(MyProject.iOS.WebViewRenderer))]
namespace MyProject.iOS
{
// Xamarin.Forms.Platform.iOS.WebViewRenderer
public class WebViewRenderer : ViewRenderer<WebView, WKWebView>
{
WKWebView wkWebView;
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (Control != null) return;
var config = new WKWebViewConfiguration();
wkWebView = new WKWebView(Frame, config) { NavigationDelegate = new MyNavigationDelegate() };
SetNativeControl(wkWebView);
}
}
public class MyNavigationDelegate : WKNavigationDelegate
{
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
//get url here
var url = webView.Url;
// webView.LoadFileUrl(url, url);
}
}
}
UPDATE:
Here's how I'm setting the source and base url for the webview in the portable project:
var result = await client.PostAsync("/embedded/pay", content);
if (result.IsSuccessStatusCode)
{
var resp = await result.Content.ReadAsStringAsync();
var html = new HtmlWebViewSource
{
Html = resp,
BaseUrl = paymentGatewayUrl
};
//Adding Cookie
CookieContainer cookies = new CookieContainer();
var domain = new Uri(html.BaseUrl).Host;
var cookie = new Cookie
{
Secure = true,
Expired = false,
HttpOnly = false,
Name = "cookie",
Expires = DateTime.Now.AddDays(10),
Domain = domain,
Path = "/"
};
cookies.Add(new Uri(html.BaseUrl), cookie);
webView.Source = html;
}
You could modify the code of Custom Renderer like following .
[assembly: ExportRenderer(typeof(WebView), typeof(MyWebViewRenderer))]
namespace xxx.iOS
{
public class MyWebViewRenderer : ViewRenderer<WebView, WKWebView>
{
WKWebView wkWebView;
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (Control != null) return;
var config = new WKWebViewConfiguration();
wkWebView = new WKWebView(Frame, config) { NavigationDelegate = new MyNavigationDelegate() };
SetNativeControl(wkWebView);
}
}
public class MyNavigationDelegate : WKNavigationDelegate
{
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
//get url here
var url = webView.Url;
//webView.LoadFileUrl();
}
}
}
I'm starting with Xamarin.Forms and what I'm trying to do is simply setting a CurrentUrl property on a custom Webview in Xamarin.Forms
In other words: When OnPageFinished method is called, I need to set the CurrentUrl property of MyWebView to the new Url.
Anyone have an idea?
Here's my main Webview:
public class MyWebView: Xamarin.Forms.WebView
{
public static readonly BindableProperty UrlProperty = BindableProperty.Create(
propertyName: "CurrentUrl",
returnType: typeof(string),
declaringType: typeof(MyWebView),
defaultValue: default(string));
public string CurrentUrl
{
get { return (string)GetValue(UrlProperty); }
set { SetValue(UrlProperty, value); }
}
}
And here's my Renderer in Project.Droid:
[assembly: ExportRenderer(typeof(MyWebView), typeof(MyProject.Droid.WebViewRenderer))]
namespace Manateq.Droid
{
public class WebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
public WebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
Control.SetWebViewClient(new Callback(Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity));
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
MyWebView myWebView = sender as MyWebView;
if (e.PropertyName == "CurrentUrl")
{
}
}
}
public class Callback : WebViewClient
{
Activity _context;
public Callback(Activity _context)
{
this._context = _context;
}
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, string url)
{
//view.LoadUrl(url);
Intent i = new Intent(Intent.ActionView, Uri.Parse(url));
_context.StartActivity(i);
return true;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
DependencyService.Get<ILoadingIndicator>().Show();
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
//element.CurrentUrl = url;
DependencyService.Get<ILoadingIndicator>().Dismiss();
}
}
}
And here's I'm using the custom web view in xaml:
<customControls:MyWebView VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" x:Name="webView"/>
You can got the value of CurrentUrl by MyWebView myWebView = e.NewElement as MyWebView; var currentUrl=myWebView.CurrentUrl; in OnElementChanged; Then you can transfer this value to Callback's constructor. In the end, you can set it in the OnPageFinished method by element.CurrentUrl = currenturl; like following code.
[assembly: ExportRenderer(typeof(MyWebView), typeof(WebViewRenderer))]
namespace WebviewDemo.Droid
{
public class WebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
public WebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
MyWebView myWebView = e.NewElement as MyWebView;
var currentUrl=myWebView.CurrentUrl;
Control.SetWebViewClient(new Callback(Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity, currentUrl));
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
MyWebView myWebView = sender as MyWebView;
if (e.PropertyName == "CurrentUrl")
{
}
}
}
public class Callback : WebViewClient
{
Activity _context;
string currenturl;
public Callback(Activity _context,string currenturl)
{
this._context = _context;
this.currenturl = currenturl;
}
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, string url)
{
//view.LoadUrl(url);
Intent i = new Intent(Intent.ActionView, Uri.Parse(url));
_context.StartActivity(i);
return true;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
DependencyService.Get<ILoadingIndicator>().Show();
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
element.CurrentUrl = currenturl;
DependencyService.Get<ILoadingIndicator>().Dismiss();
}
}
}
Do you want to achieve the result like this GIF?
If so, you can load the current url in the OnPageFinished method.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Webkit;
using Android.Widget;
using WebviewDemo;
using WebviewDemo.Droid;
using Xamarin.Forms;
[assembly: ExportRenderer(typeof(MyWebView), typeof(WebViewRenderer))]
namespace WebviewDemo.Droid
{
public class WebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
public WebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
MyWebView myWebView = e.NewElement as MyWebView;
var currentUrl=myWebView.CurrentUrl;
Control.SetWebViewClient(new Callback(Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity, currentUrl));
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
MyWebView myWebView = sender as MyWebView;
if (e.PropertyName == "CurrentUrl")
{
}
}
}
public class Callback : WebViewClient
{
Activity _context;
string currenturl;
public Callback(Activity _context,string currenturl)
{
this._context = _context;
this.currenturl = currenturl;
}
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, string url)
{
//if (!loadingFinished)
//{
// redirect = true;
//}
//loadingFinished = false;
//view.LoadUrl(currenturl);
//Console.WriteLine("Loading web view...");
return true;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
if (url.Equals("https://www.google.com/"))
{
view.LoadUrl(currenturl);
}
}
//public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, string url)
//{
// //view.LoadUrl(url);
// // Intent i = new Intent(Intent.ActionView, Uri.Parse(url));
// // _context.StartActivity(i);
// view.LoadUrl(url);
// isRedirected = true;
// return true;
//}
//public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
//{
// base.OnPageStarted(view, url, favicon);
// if (!isRedirected)
// {
// //Do something you want when starts loading
// }
// isRedirected = false;
// // DependencyService.Get<ILoadingIndicator>().Show();
//}
//public override void OnPageFinished(Android.Webkit.WebView view, string url)
//{
// base.OnPageFinished(view, url);
// if (!isRedirected)
// {
// //Do something you want when finished loading
// }
// // element.CurrentUrl = currenturl;
// // DependencyService.Get<ILoadingIndicator>().Dismiss();
//}
}
}
In the xaml. You can bind the value for CurrentUrl, Or just set it directly.
<customControls:MyWebView VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" x:Name="webView"/>
Xaml background code.
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
webView.Source = "https://www.google.com";
webView.CurrentUrl = "https://www.baidu.com";
}
}
The answer provided by Leon is good. But I found an easier way to do it. Here's what I've done.
In the Callback class that extends WebviewClient I've edited OnPageStarted and OnPageFinished to be as following:
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
DependencyService.Get<ILoadingIndicator>().Show();
base.OnPageStarted(view, url, favicon);
var args = new WebNavigatingEventArgs(WebNavigationEvent.NewPage, new UrlWebViewSource { Url = url }, url);
_renderer.ElementController.SendNavigating(args);
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
DependencyService.Get<ILoadingIndicator>().Dismiss();
base.OnPageFinished(view, url);
var source = new UrlWebViewSource { Url = url };
var args = new WebNavigatedEventArgs(WebNavigationEvent.NewPage, source, url, WebNavigationResult.Success);
_renderer.ElementController.SendNavigated(args);
}
As for _rendered its: ElementController => Element; and it exist in the WebViewRenderer class.
When adding these line of codes, the Navigated event is being hit normally. So when that happens we can simply use the url parameter that exist in Navigated event.
I Have a custom hybridWebView for android and ios to load the URL. What I required is to pass a callback to the content page once the URL has completed the loading. Code as below, help would much appreciate.
Content Page
public partial class ConveyancingLeadPage : ContentPage
{
DashboardViewModel viewmodel;
StorageService storage = new StorageService();
public ConveyancingLeadPage()
{
InitializeComponent();
GetUserAvatar();
}
protected async override void OnAppearing()
{
// I need the callback to be execute here
customView.weblink = viewmodel.BrokerData.config.conveyancing.listing_webview;
}
}
Android HybridView
[assembly: ExportRenderer(typeof(HCHybridWebview), typeof(HCHybridWebviewRendererAndroid))]
namespace HashChing.Droid.CustomRenderers
{
public class HCHybridWebviewRendererAndroid : ViewRenderer<HCHybridWebview, Android.Webkit.WebView>
{
Context _context;
public HCHybridWebviewRendererAndroid(Context context) : base(context)
{
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<HCHybridWebview> e)
{
base.OnElementChanged(e);
const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
if (Control == null)
{
//Do something
if (e.NewElement != null)
{
//Load URL
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
var hybridWebView = e.NewElement as HCHybridWebview;
if (hybridWebView != null)
{
hybridWebView.RefreshView += LoadUrl;
}
}
Load URL
public void LoadUrl(object sender, EventArgs e)
{
Control.LoadUrl(webView.weblink, headers);
}
Once the URL been loaded it will navigate to this method in the same class, and this is where I want to pass a callback TO my content page once the loading is completed inside the "OnPageFinished" method. Help would much appreciate.
public class JavascriptWebViewClient : WebViewClient
{
string _javascript;
public JavascriptWebViewClient(string javascript)
{
_javascript = javascript;
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(_javascript, null);
}
}
you could use MessagingCenter to send and get the callback:
in your ContentPage,for example Page1.xaml.cs
public Page1 ()
{
InitializeComponent ();
//here you could get the callback,and arg = "this is call back"
MessagingCenter.Subscribe<Page1,string>(this,"callback", (send, arg) =>
{
Console.WriteLine(arg);
});
}
then in your OnPageFinished method:
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(_javascript, null);
//send the callback content,Parameters can be defined by yourself
MessagingCenter.Send<Page1,string>(new Page1(), "callback","this is call back");
}
more information:MessagingCenter
create your own event LoadCompleted in your customrender and invoke it from your custom class once load is finish.
And in your JavascriptWebViewClient class you can subscribe to that event and do whatever you want do do at that time.
in case you need it: Events in c#
public partial class ConveyancingLeadPage : ContentPage
{
protected async override void OnAppearing()
{
customView.weblink = viewmodel.BrokerData.config.conveyancing.listing_webview;
// I need the callback to be execute here
customView.LoadCompleted+=LoadCompleted;
}
}
I have an app which I am converting from iOS only to iOS & Droid using MVVMCross.
In my current app I have a map view that uses a UISearchController that allows the user to search for locations nearby. This is based on the Xamarin example and works fine:
Xamarin Map Example
For the conversion I have:
a MapView bound to a MapViewModel.
A search service which is injected into MapViewModel.
Created a UISearchController and bound the search text to a property on the MapViewModel.
When the text is updated the search is called and the results are retrieved. What I am struggling with is how to bind the results back to a SearchResultsView as this is presented by the UISearchController.
Can anyone give me advice or point me in the right direction to solve this.
I have the code snippet below to give an idea of what I have relied so far.
[MvxFromStoryboard]
public partial class MapView : MvxViewController<MapViewModel>
{
public MapView(IntPtr handle) : base(handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
var searchResultsController = new SearchResultsView();
//Not sure if this is required
//var searchUpdater.UpdateSearchResults += searchResultsController.Search;
var searchController = new UISearchController(searchResultsController)
{
//Nore sure if this is required
//SearchResultsUpdater = searchUpdater
};
searchController.SearchBar.SizeToFit();
searchController.SearchBar.SearchBarStyle = UISearchBarStyle.Minimal;
searchController.SearchBar.Placeholder = "Enter a search query";
searchController.HidesNavigationBarDuringPresentation = false;
DefinesPresentationContext = true;
NavigationItem.TitleView = searchController.SearchBar;
//Bind to View Model
var set = this.CreateBindingSet<MapView, MapViewModel>();
set.Bind(searchController.SearchBar).To(vm => vm.SearchQuery);
set.Apply();
}
}
public class SearchResultsUpdator : UISearchResultsUpdating
{
public event Action<string> UpdateSearchResults = delegate { };
public override void UpdateSearchResultsForSearchController(UISearchController searchController)
{
this.UpdateSearchResults(searchController.SearchBar.Text);
}
}
[MvxFromStoryboard]
public partial class SearchResultsView : MvxTableViewController<SearchResultsViewModel>
{
public SearchResultsView() { }
public SearchResultsView(IntPtr handle) : base(handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
var source = new SearchResultsTableViewSource(TableView);
TableView.Source = source;
var set = this.CreateBindingSet<SearchResultsView, SearchResultsViewModel>();
set.Bind(source).To(vm => vm.Results);
set.Apply();
}
}
[MvxFromStoryboard]
public partial class SearchResultsView : MvxTableViewController<SearchResultsViewModel>
{
public SearchResultsView() { }
public SearchResultsView(IntPtr handle) : base(handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
var source = new SearchResultsTableViewSource(TableView);
TableView.Source = source;
var set = this.CreateBindingSet<SearchResultsView, SearchResultsViewModel>();
set.Bind(source).To(vm => vm.Results);
set.Apply();
}
}
I have posted this in case someone else is looking for an example. I decided the best way to do this was to let iOS handle the search view controller for the results. Code follows. Feel free to correct or suggest a better alternative
View
[MvxFromStoryboard]
public partial class MapView : MvxViewController
{
UISearchController _searchController;
SearchResultsViewController _searchResultsController;
private IDisposable _searchResultsUpdateSubscription;
private IMvxInteraction _searchResultsUpdatedInteraction;
public IMvxInteraction SearchResultsUpdatedInteraction
{
get => _searchResultsUpdatedInteraction;
set
{
if (_searchResultsUpdateSubscription != null)
{
_searchResultsUpdateSubscription.Dispose();
_searchResultsUpdateSubscription = null;
}
_searchResultsUpdatedInteraction = value;
if (_searchResultsUpdatedInteraction != null)
{
_searchResultsUpdateSubscription = _searchResultsUpdatedInteraction.WeakSubscribe(OnSearchResultsUpdated);
}
}
}
private void OnSearchResultsUpdated(object sender, EventArgs e)
{
_searchResultsController.SearchResults = Results;
_searchResultsController.ReloadSearchTable();
}
public List<Placemark> Results { get; set; }
public MapView(IntPtr handle) : base(handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
//Bind to View Model
var set = this.CreateBindingSet<MapView, MapViewModel>();
set.Bind(_searchController.SearchBar).To(vm => vm.SearchQuery);
set.Bind(this).For(v => v.Results).To(vm => vm.Results);
set.Bind(this).For(v => v.SearchResultsUpdatedInteraction).To(vm => vm.SearchResultsUpdatedInteraction).OneWay();
set.Apply();
}
ViewModel
public class MapViewModel : MvxViewModel
{
readonly ILocationService _locationService;
private MvxInteraction _searchResultsUpdatedInteraction = new MvxInteraction();
public IMvxInteraction SearchResultsUpdatedInteraction => _searchResultsUpdatedInteraction;
public MapViewModel(ILocationService locationService)
{
_locationService = locationService;
}
//***** Properties *****
private List<Placemark> _results;
public List<Placemark> Results
{
get => _results;
set
{
_results = value;
RaisePropertyChanged();
}
}
private string _searchQuery;
public string SearchQuery
{
get => _searchQuery;
set
{
_searchQuery = value;
//Task.Run(UpdateResultsAsync).Wait();
RaisePropertyChanged();
UpdateResultsAsync();
}
}
//***** Privates *****
private async Task UpdateResultsAsync()
{
Results = await _locationService.SearchForPlacesAsync(_searchQuery);
_searchResultsUpdatedInteraction.Raise();
}
}
SearchResultsViewController
public class SearchResultsViewController : UITableViewController
{
static readonly string mapItemCellId = "mapItemCellId";
public List<Placemark> SearchResults { get; set; }
public SearchResultsViewController()
{
SearchResults = new List<Placemark>();
}
public override nint RowsInSection(UITableView tableView, nint section)
{
return SearchResults == null ? 0 : SearchResults.Count;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var cell = tableView.DequeueReusableCell(mapItemCellId);
if (cell == null)
cell = new UITableViewCell();
cell.TextLabel.Text = SearchResults[indexPath.Row].FeatureName;
return cell;
}
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
//Do stuff here
}
public void ReloadSearchTable()
{
this.TableView.ReloadData();
}
}
I tend to dislike posting dozens of lines of code and assuming the community at large is interested in untangling my mess. In this case I've exercised everything I can think to search on Google, traced through Glimpse, and Firebug/Fiddler, and what I'm left with is an occasionally working behavior, which is particularly annoying to debug. So, I'm calling out for help.
Here's the gist: I've got a series of classes that handle MVC routes that are otherwise not found (and would produce a 404 error) thanks to #AndrewDavey. I'm attempting to intercept the 404 and show data-driven content where any exists. It all works until I refresh the page. The request works on the first load, but it never fires again after that.
If you're bored or have an itch, the entire code block is below.
Setup goes like this:
Add WebActivator via NuGet
In your AppStart folder add a cs file with the code below
Add a "PageContext" connection string to your web.config
Run the app, the default MVC screen shows up
Now add "/abc" to the end of the url (i.e http://localhost/abc)
A cshtml view, stored in the database, will render.
Change the view's markup in the database and reload the page. Notice no change in your browser.
the /abc route assumes you have a record in the database with the following
Path: "~/abc/index.cshtml"
View: "#{ Layout = null;}<!doctype html><html><head><title>abc</title></head><body><h2>About</h2></body></html>"
I've got no idea why the first request works and subsequent requests don't hit break points and serve up stale content.
My suspicions are:
Some voodoo with the VirtualFile
Something cached (but where?)
A misconfigured handler
Thanks for the help - here's the code (as I shamefully tuck my tail for posting this much code).
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Entity;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Caching;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using Microsoft.Web.Infrastructure.DynamicModuleHelper;
using SomeCms;
[assembly: WebActivator.PreApplicationStartMethod(typeof(Sample.Web.App_Start.cms), "PreStart")]
namespace Sample.Web.App_Start
{
public static class cms
{
public static void PreStart()
{
DynamicModuleUtility.RegisterModule(typeof(InstallerModule));
}
}
}
namespace SomeCms
{
class ActionInvokerWrapper : IActionInvoker
{
readonly IActionInvoker actionInvoker;
public ActionInvokerWrapper(IActionInvoker actionInvoker)
{
this.actionInvoker = actionInvoker;
}
public bool InvokeAction(ControllerContext controllerContext, string actionName)
{
if (actionInvoker.InvokeAction(controllerContext, actionName))
{
return true;
}
// No action method was found.
var controller = new CmsContentController();
controller.ExecuteCmsContent(controllerContext.RequestContext);
return true;
}
}
class ControllerFactoryWrapper : IControllerFactory
{
readonly IControllerFactory factory;
public ControllerFactoryWrapper(IControllerFactory factory)
{
this.factory = factory;
}
public IController CreateController(RequestContext requestContext, string controllerName)
{
try
{
var controller = factory.CreateController(requestContext, controllerName);
WrapControllerActionInvoker(controller);
return controller;
}
catch (HttpException ex)
{
if (ex.GetHttpCode() == 404)
{
return new CmsContentController();
}
throw;
}
}
static void WrapControllerActionInvoker(IController controller)
{
var controllerWithInvoker = controller as Controller;
if (controllerWithInvoker != null)
{
controllerWithInvoker.ActionInvoker = new ActionInvokerWrapper(controllerWithInvoker.ActionInvoker);
}
}
public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
{
return factory.GetControllerSessionBehavior(requestContext, controllerName);
}
public void ReleaseController(IController controller)
{
factory.ReleaseController(controller);
}
}
class InstallerModule : IHttpModule
{
static bool installed;
static readonly object installerLock = new object();
public void Init(HttpApplication application)
{
if (installed)
{
return;
}
lock (installerLock)
{
if (installed)
{
return;
}
Install();
installed = true;
}
}
static void Install()
{
Database.SetInitializer(new CreateDatabaseIfNotExists<PageContext>());
HostingEnvironment.RegisterVirtualPathProvider(new ExampleVirtualPathProvider());
WrapControllerBuilder();
AddNotFoundRoute();
AddCatchAllRoute();
}
static void WrapControllerBuilder()
{
ControllerBuilder.Current.SetControllerFactory(new ControllerFactoryWrapper(ControllerBuilder.Current.GetControllerFactory()));
}
static void AddNotFoundRoute()
{
// To allow IIS to execute "/cmscontent" when requesting something which is disallowed,
// such as /bin or /add_data.
RouteTable.Routes.MapRoute(
"CmsContent",
"cmscontent",
new { controller = "CmsContent", action = "CmsContent" }
);
}
static void AddCatchAllRoute()
{
RouteTable.Routes.MapRoute(
"CmsContent-Catch-All",
"{*any}",
new { controller = "CmsContent", action = "CmsContent" }
);
}
public void Dispose() { }
}
public class CmsContentController : IController
{
public void Execute(RequestContext requestContext)
{
ExecuteCmsContent(requestContext);
}
public void ExecuteCmsContent(RequestContext requestContext)
{
//new CmsContentViewResult().ExecuteResult(new ControllerContext(requestContext, new FakeController()));
new CmsContentViewResult().ExecuteResult(new ControllerContext(requestContext, new FakeController()));
}
// ControllerContext requires an object that derives from ControllerBase.
// NotFoundController does not do this.
// So the easiest workaround is this FakeController.
class FakeController : Controller { }
}
public class CmsContentHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
var routeData = new RouteData();
routeData.Values.Add("controller", "CmsContent");
var controllerContext = new ControllerContext(new HttpContextWrapper(context), routeData, new FakeController());
var cmsContentViewResult = new CmsContentViewResult();
cmsContentViewResult.ExecuteResult(controllerContext);
}
public bool IsReusable
{
get { return false; }
}
// ControllerContext requires an object that derives from ControllerBase.
class FakeController : Controller { }
}
public class CmsContentViewResult : ViewResult
{
public CmsContentViewResult()
{
ViewName = "index";
}
public override void ExecuteResult(ControllerContext context)
{
var request = context.HttpContext.Request;
if (request != null && request.Url != null)
{
var url = request.Url.OriginalString;
ViewData["RequestedUrl"] = url;
ViewData["ReferrerUrl"] = (request.UrlReferrer != null && request.UrlReferrer.OriginalString != url)
? request.UrlReferrer.OriginalString
: null;
}
base.ExecuteResult(context);
}
}
public class ExampleVirtualPathProvider : VirtualPathProvider
{
private readonly List<SimpleVirtualFile> virtualFiles = new List<SimpleVirtualFile>();
public ExampleVirtualPathProvider()
{
var context = new PageContext();
var pages = context.Pages.ToList();
foreach (var page in pages)
{
virtualFiles.Add(new SimpleVirtualFile(page.Path));
}
}
public override bool FileExists(string virtualPath)
{
var files = (from f in virtualFiles
where f.VirtualPath.Equals(virtualPath, StringComparison.InvariantCultureIgnoreCase) ||
f.RelativePath.Equals(virtualPath, StringComparison.InvariantCultureIgnoreCase)
select f)
.ToList();
return files.Count > 0 || base.FileExists(virtualPath);
}
private class SimpleVirtualFile : VirtualFile
{
public SimpleVirtualFile(string filename) : base(filename)
{
RelativePath = filename;
}
public override Stream Open()
{
var context = new PageContext();
var page = context.Pages.FirstOrDefault(p => p.Path == RelativePath);
return new MemoryStream(Encoding.ASCII.GetBytes(page.View), false);
}
public string RelativePath { get; private set; }
}
private class SimpleVirtualDirectory : VirtualDirectory
{
public SimpleVirtualDirectory(string virtualPath)
: base(virtualPath)
{
}
public override IEnumerable Directories
{
get { return null; }
}
public override IEnumerable Files
{
get
{
return null;
}
}
public override IEnumerable Children
{
get { return null; }
}
}
public override VirtualFile GetFile(string virtualPath)
{
var files = (from f in virtualFiles
where f.VirtualPath.Equals(virtualPath, StringComparison.InvariantCultureIgnoreCase) ||
f.RelativePath.Equals(virtualPath, StringComparison.InvariantCultureIgnoreCase)
select f).ToList();
return files.Count > 0
? files[0]
: base.GetFile(virtualPath);
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
return IsPathVirtual(virtualPath) ? null : base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
private bool IsPathVirtual(string virtualPath)
{
var checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return
virtualFiles.Any(f => checkPath.StartsWith(virtualPath, StringComparison.InvariantCultureIgnoreCase)) ||
virtualFiles.Any(f => checkPath.Replace("~", "").StartsWith(virtualPath, StringComparison.InvariantCultureIgnoreCase));
}
public override bool DirectoryExists(string virtualDir)
{
return IsPathVirtual(virtualDir) || Previous.DirectoryExists(virtualDir);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return IsPathVirtual(virtualDir)
? new SimpleVirtualDirectory(virtualDir)
: Previous.GetDirectory(virtualDir);
}
}
public class ContentPage
{
public int Id { get; set; }
public string Path { get; set; }
public string View { get; set; }
}
public class PageContext : DbContext
{
public DbSet<ContentPage> Pages { get; set; }
}
}
This question turns out to be a non-issue. My oversight of the cache dependency in the virtual path provider is returning null for virtual paths. As such, the view is cached indefinitely.
The solution is to use a custom cache dependency provider that expires immediately.
public class NoCacheDependency : CacheDependency
{
public NoCacheDependency()
{
NotifyDependencyChanged(this, EventArgs.Empty);
}
}
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
{
return IsPathVirtual(virtualPath) ? new NoCacheDependency() : base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}