Dynamically made pages don't load actual page content/code - c#

I'm trying to create a modular way of loading pages, each page having it's own navigation menu item in the main window header and a separate frame to keep them always loaded in memory (and to play fancy animations etc.)
Here's the class that contains the loaded page.
public sealed class PageContainer : Frame
{
public string Title;
public PageContainer(string Page, bool CustomUri = false)
{
Visibility = System.Windows.Visibility.Collapsed;
NavigationUIVisibility = NavigationUIVisibility.Hidden;
Title = Page;
if(!CustomUri)
{
Navigate($"pack://lotus:,,,/Views/UserPages/{Page}.xaml", UriKind.Relative);
}
else
{
//todo
}
Console.WriteLine($"Navigation Item created {Title} with ?CustomUri: {CustomUri}");
}
public bool Visible
{
get => Visibility == System.Windows.Visibility.Visible;
set => Visibility = value ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;
}
}
And here's how I'm creating the PageContainer(s)
Animations.PageTransitionAnimations AnimationLibrary;
public double _SlideAnimationLenght = 250;
public MainWindow()
{
InitializeComponent();
//Initialize Animation Library
AnimationLibrary = new Animations.PageTransitionAnimations(this);
InitializePage("Library");
InitializePage("Preferences");
InitializePage("Plugins");
InitializePage("Information");
foreach(PageContainer pp in LoadedContainers)
{
pp.Visible = true;
pp.BeginAnimation(MarginProperty, AnimationLibrary.ToLeft);
}
}
//Load a page and add it to navigation bar, if NoHeader, then don't add it to navigation
private void InitializePage(string page, bool noHeader = false)
{
if(!noHeader)
{
//Add page to navigation bar and set style
NavigationHeader.Children.Add(new NavigationItem(page)
{
Style = FindResource("HeaderMenu") as System.Windows.Style
});
}
//Add Page to PageContainer and Initialize it
PageContainer _page = new PageContainer(page);
_containers.Add(_page);
PagesContainer.Children.Add(_page);
}
//Public LoadedContainers, no setter
private List<PageContainer> _containers = new List<PageContainer>();
public List<PageContainer> LoadedContainers => _containers;
What happens is that the navigation in PageContainer actually succeeds the navigation event, but frames don't report any content, page code doesn't execute and there's no design.
Any suggestions? Thanks.
Edit:
Pages themselves have no problems, because I load them manually, they do work. Also, here's the link to full code: https://github.com/FaithLV/lotus

There was an issue with how I was navigating to the page.
Turns out, the "application" in URI wasn't context sensitive and actually is used as is.
Here's the correct way to navigation:
Navigate(new Uri($"pack://application:,,,/Views/UserPages/{Page}.xaml"), UriKind.Absolute);

Related

Adding UIImageView to UIAlertController

Goal: add an image above an UIAlertController's Title label by subclassing UIAlertController and adding new line characters, \n, to the title string to make space for the UIImageView
Desire
Current
As one can see, able to add the image to the UIAlertController successfully but the image is not being spacing/placed above the Title. It appears to be adding to the center of the alert. How to space the image correctly above the UIAlertController title?
Current code:
namespace XamarinFormsApp1.Extensions
{
public class AlertController : UIAlertController
{
private string originalTitle;
private string spaceAdjustedTitle;
private UIImageView imageView = null;
private CoreGraphics.CGSize previousImgViewSize
= CoreGraphics.CGSize.Empty;
public override UIAlertControllerStyle PreferredStyle
{
get
{
return UIAlertControllerStyle.Alert;
}
}
public override string Title
{
get
{
return originalTitle;
}
set
{
if (Title != spaceAdjustedTitle ||
string.IsNullOrEmpty(Title) ||
string.IsNullOrEmpty(spaceAdjustedTitle))
{
originalTitle = value;
}
}
}
public void setTitleImage(UIImage image)
{
if (this.imageView == null)
{
UIImageView imageView = new UIImageView(image);
this.View.AddSubview(imageView);
this.imageView = imageView;
return;
}
imageView.Image = image;
}
public override void ViewDidLayoutSubviews()
{
if (imageView == null)
{
base.ViewDidLayoutSubviews();
return;
}
// Adjust title if image size has changed
if (previousImgViewSize != imageView.Bounds.Size)
{
previousImgViewSize = imageView.Bounds.Size;
adjustTitle(imageView);
}
// Position `imageView`
var linesCount = newLinesCount(imageView);
var padding = Constants.Padding(PreferredStyle);
var x = View.Bounds.Width / 2.0;
var y = padding + linesCount * lineHeight / 2.0;
CoreGraphics.CGPoint cgPoint = imageView.Center;
cgPoint.X = (nfloat)x;
cgPoint.Y = (nfloat)y;
imageView.Center = cgPoint;
base.ViewDidLayoutSubviews();
}
private void adjustTitle(UIImageView imageView)
{
var linesCount = (int)newLinesCount(imageView);
var lines = Enumerable
.Range(1, linesCount)
.Select(i => "\n")
.Aggregate((c, n) => $"{c}{n}");
spaceAdjustedTitle = lines + (originalTitle ?? "");
Title = spaceAdjustedTitle;
}
private double newLinesCount(UIImageView imageView)
{
return Math.Ceiling(
imageView.Bounds.Height / lineHeight);
}
private float lineHeight
{
get
{
UIFontTextStyle style = this.PreferredStyle
== UIAlertControllerStyle.Alert
? UIFontTextStyle.Headline
: UIFontTextStyle.Callout;
return (float)UIFont
.GetPreferredFontForTextStyle(style)
.PointSize;
}
}
struct Constants
{
static float paddingAlert = 22;
static float paddingSheet = 11;
public static float Padding(UIAlertControllerStyle style)
{
return style == UIAlertControllerStyle.Alert
? Constants.paddingAlert
: Constants.paddingSheet;
}
}
}
}
Note: Credit to #stringCode for image and swift solution, see.
UIAlertViewController is not meant to be subclassed.
An extract from the documentation says:
You could still get the UI you desire by using a UIViewController with transparency on the View and a subview with the layout you desire.
You would need to also set these two properties: ModalTransitionStyle and ModalPresentationStyle to UIModalTransitionStyle.CrossDissolve and UIModalPresentationStyle.OverCurrentContext respectively if you want your custom UIAlertController to behaves the same as a UIAlertController
Update:
This is what I meant you could do:
In the Main.Storyboard drop a UIViewController and update the design as you wish. Following the image you posted above I created the UI as seen below:
That's an Image, 2 UILabels for the Title and Message and 3 buttons for the 3 different actions (Default, Destroy, Cancel). All these controls are inside a UIView with White background. For the example I called it ContentView
Adding the 3 button on the UI seems to be the easiest way to work with this and then hide/show them when you are about to present your alert. You could also create the buttons on the fly based on the actions you wanna show. This is up to you.
Create a ViewController Class, I called it NiceAlertController, and assign it to the ViewController in the Storyboard. Also, make sure to create back properties (Outlets) for all the UIControls (Label, Button, Image, etc) so you can access it from the ViewController class.
Here more information about how to work with iOS Storyboard on the designer
Now in your class you will need to add the code to make it work.
In your class to make the view transparent you will need to add this to your ViewDidLoad method:
public override void ViewDidLoad()
{
base.ViewDidLoad();
this.View.BackgroundColor = UIColor.Clear.ColorWithAlpha(0.2f);
this.View.Opaque = false;
}
Also, we could mimic the way UIAlertControllers are created and create our method like that one:
public static NiceAlertController Create(string title, string message, UIImage image = null)
{
//New instance of your ViewController UI
var storyboard = UIStoryboard.FromName("Main", NSBundle.MainBundle);
var alertController = storyboard.InstantiateViewController(nameof(NiceAlertController)) as NiceAlertController;
//Using the same transition and presentation style as the UIAlertViewController
alertController.ModalTransitionStyle = UIModalTransitionStyle.CrossDissolve;
alertController.ModalPresentationStyle = UIModalPresentationStyle.OverCurrentContext;
//This assumes your properties are called Title, Message and Image
alertController.Title = title;
alertController.Message = message;
alertController.Image = image;
return alertController;
}
The 3 properties used above (Title, Message and Image) looks like this:
public new string Title { get; private set; }
public string Message { get; private set; }
public UIImage Image { get; private set; }
Why these properties? because by the time you create the class the Controls on the view are not yet available. They will be available only after the View is loaded. This is why we will need to add other changes like the one below.
Here we are setting the values to the Controls on the UI
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
this.titleLabel.Text = Title;
this.messageLabel.Text = Message;
//If you don't set an image while Create, it will use the default image you set on the Designer.
if (Image != null)
{
this.imageView.Image = Image;
}
}
Now from any other ViewController you can call this ViewController as you would call an AlertViewController:
private void ShowMyAlertController()
{
var alert = NiceAlertController.Create("Hello", "My nice alert controller");
this.PresentViewController(alert, true, null);
}
And it should look like this:
To handle the Actions (What to do when the buttons are tapped) you could create specific methods like:
public void AddDefaultAction(string title, Action action)
{
//Logic here
}
public void AddCancelAction(string title, Action action)
{
//Logic here
}
public void AddDestructiveAction(string title, Action action)
{
//Logic here
}
Hope this gives you the idea of how to create custom UIViewcontroller and make it look like a UIAlertViewController.
Hope this helps.-

Attached Property for Binding to WebBrowser not working

I have been looking for a way to get the HTML out of a WPF WebBrowser control. The two best options I have found are to bind a customer attached property to the property in the application or to build a new control from the WebBrowser control. Considering my level of knowledge and the fact that (as of now I really only need this one time) I chose the first. I even considered breaking MVVM style and using code-behind but I decided not to give up in the binding.
I found several examples on creating the attached property, I finally chose this one, from here Here:
namespace CumminsInvoiceTool.HelperClasses
{
public static class WebBrowserExtentions
{
public static readonly DependencyProperty DocumentProperty =
DependencyProperty.RegisterAttached("Document", typeof(string), typeof(WebBrowserExtentions), new UIPropertyMetadata(null, DocumentPropertyChanged));
public static string GetDocument(DependencyObject element)
{
return (string)element.GetValue(DocumentProperty);
}
public static void SetDocument(DependencyObject element, string value)
{
element.SetValue(DocumentProperty, value);
}
public static void DocumentPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
WebBrowser browser = target as WebBrowser;
if (browser != null)
{
string document = e.NewValue as string;
browser.NavigateToString(document);
}
}
}
}
I also added the following to the xaml for the WebBrowser control (I have tried both with and without the "Path=" in the xaml:
<WebBrowser local:WebBrowserExtentions.Document="{Binding Path=PageCode}" Source="https://www.cummins-distributors.com/"/>
My View has a tab control one tab has the WebBrowser control and another tab has a textbox. When I click the get code the viewModel runs a function to set property bound to the textbox to the string the attached property of the WebBrowser is bound to. Below is the code of my ViewModel.
namespace CumminsInvoiceTool.ViewModels
{
class ShellViewModel : Screen
{
private string _browserContent;
public string BrowserContent
{
get { return _browserContent; }
set {
_browserContent = value;
NotifyOfPropertyChange(() => BrowserContent);
}
}
private string _pageCode;
public string PageCode
{
get { return _pageCode; }
set {
_pageCode = value;
NotifyOfPropertyChange(() => PageCode);
}
}
public void StartProgressCommand()
{
}
public void GetContent()
{
if (!string.IsNullOrEmpty(PageCode))
{
BrowserContent = PageCode;
}
else
{
MessageBox.Show("There is no cintent to show", "No content Error", MessageBoxButton.OK);
}
}
}
}
The application compiles and runs but when I click "Get Code" I am getting the messagebox for "PageCode" is empty.
When I set a break point at the beginning of the function for the button, the PageCode string is showing "null".
Is this an issue because I am using Caliburn.Micro or am I missing something else?
------- EDIT for comments ----------
The button calls GetContent() in the "ShellViewModel" code above. I know the button is bound and working because the app is showing the custom messagebox I have set up to let me know when "pageCode" is null or empty.
The textbox looks like:
<TextBox x:Name="BrowserContent"/>

Xamarin.Forms.WebView.Navigating event raised on iOS for internal navigation

Let's say you want to prevent the user from navigating away from your Xamarin.Forms.WebView to an external page.
public App ()
{
var webView = new WebView
{
Source = new HtmlWebViewSource
{
Html = "<h1>Hello world</h1><a href='http://example.com'>Can't escape!</a><iframe width='420' height='315' src='https://www.youtube.com/embed/oHg5SJYRHA0' frameborder='0' allowfullscreen></iframe>"
}
};
webView.Navigating += WebView_Navigating;
MainPage = new ContentPage {
Content = webView
};
}
private void WebView_Navigating(object sender, WebNavigatingEventArgs e)
{
// we don't want to navigate away from our page
// open it in a new page instead etc.
e.Cancel = true;
}
This works fine on Windows and Android. But on iOS, it doesn't load at all!
On iOS, the Navigating event gets raised even when loading the source from a HtmlWebViewSource, with a URL that looks something like file:///Users/[user]/Library/Developer/CoreSimulator/Devices/[deviceID]/data/Containers/Bundle/Application/[appID]/[appName].app/
Alright, so you can get around that with something like this:
private void WebView_Navigating(object sender, WebNavigatingEventArgs e)
{
if (e.Url.StartsWith("file:") == false)
e.Cancel = true;
}
The page finally loads on iOS. Yay. But wait! The embedded YouTube video doesn't load! That's because the Navigating event gets raised for the internal navigation of embedded resources like iframes and even external scripts (like Twitter's <script charset="utf-8" type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>), but only on iOS!
I couldn't find a way to determine if the Navigating event was raised from internal navigation or because the user clicked a link.
How to get around this?
I am not sure if it is possible to detect in Xamarin Forms out of the box but the navigation type is easily determined using a custom renderer. In your custom iOS renderer, assign a WebViewDelegate and within that Delegate class, override ShouldStartLoad() like so:
public class CustomWebViewRenderer : WebViewRenderer {
#region Properties
public CustomWebView CustomWebViewItem { get { return Element as CustomWebView; } }
#endregion
protected override void OnElementChanged(VisualElementChangedEventArgs e) {
base.OnElementChanged(e);
if(e.OldElement == null) {
Delegate = new CustomWebViewDelegate(); //Assigning the delegate
}
}
}
internal class CustomWebViewDelegate : UIWebViewDelegate {
public override bool ShouldStartLoad(UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType) {
if(navigationType == UIWebViewNavigationType.LinkClicked) {
//To prevent navigation when a link is click, return false
return false;
}
return true;
}
}
You could also surface a bool property or even an enum back up to your Xamarin Forms WebView which would say whether the Navigating event was from a link being clicked or from something else, though a custom renderer would be needed for that as well.
private bool isNavigated = false;
public CustomWebView()
{
if (Device.OS == TargetPlatform.Android)
{
// always true for android
isNavigated = true;
}
Navigated += (sender, e) =>
{
isNavigated = true;
};
Navigating += (sender, e) =>
{
if (isNavigated)
{
try
{
var uri = new Uri(e.Url);
Device.OpenUri(uri);
}
catch (Exception)
{
}
e.Cancel = true;
}
};
}

Toggle iOS NavigationPage.SetHasNavigationBar with TabbedPage results in hiding the tabs

Anyone tried this scenario before? I'm talking about latest Xamarin forms 2 targetting iOS.
I have a TabbedPage with 4 tabs, when the user looks at the first 2, there should not be a navigationbar - I got this working by setting the NavigationPage.SetHasNavigationBar(this, false) in the constructor of the TabbedPage.
Now by using the ExtendedTabbedPage of Xamarin forms labs, I can hook up to the change tab event called OnCurrentPageChanged() and in here I verify if I'm on one of the last 2 tabs and toggle the NavigationPage.SetHasNavigationBar(this, true).
This actually works great, except for 1 small detail. When the navigation bar gets shown, tab page shifts down and it hides the tabs at the bottom, so the user is unable to switch tabs at that moment.
When I set NavigationPage.SetHasNavigationBar(this, true) in the constructor of the tabbedpage and leave it for all tabs, the result is ok. Meaning I get a navigationheader and tabs visible on each tab.
Ok found a solution myself.
So the original setup was as follows, in the App.xaml.cs I initiated the Navigation through following code
NavigationPage navigationPage = new NavigationPage(new MainPage());
MainPage is a TabbedPage containing several children.
What I did was toggling the NavigationBar of that MainPage when switching from one child page to another
NavigationPage.SetHasNavigationBar(this, false);
NavigationPage.SetHasNavigationBar(this, true);
But that resulted in the child pages not being resized, leaving the tabbar at the bottom of the page invisible when the navigationbar came visible.
So the actual solution is to not set the MainPage as a NavigationPage, but wrap each child in a NavigationPage!
This way each child can have it's own NavigationBar an that keeps the size in reference to the MainPage correct.
So adding each child would be like this
this.Children.Add (new NavigationPage (new DiscoverPage ()){ Title = "Discover" });
Took me a while to figure this out because I normally would add the children inside the actual xaml of the MainPage but that way you can't wrap them in a NavigationPage, hence why I wrapped the MainPage itself inside the NavigationPage!
It could be some bug in computing correct size of tabbed page in NavigationPage when toggling NavagationBar visibility.
In order that I need that tabbed page must be in NavigationPage I came up with the next solution:
public class BaseNavigationPage : NavigationPage
{
public BaseNavigationPage() : base()
{
this.BarTextColor = Color.White;
}
public BaseNavigationPage(Page page) : base(page)
{
this.BarTextColor = Color.White;
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == nameof(CurrentPage))
{
Current = CurrentPage;
}
}
private void CurrentPage_PropertyChanging(object sender, PropertyChangingEventArgs e)
{
if (e.PropertyName == NavigationPage.HasNavigationBarProperty.PropertyName && _bounds == Rectangle.Zero)
{
_bounds = CurrentPage.Bounds;
}
}
private void Current_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == NavigationPage.HasNavigationBarProperty.PropertyName)
{
var has = NavigationPage.GetHasNavigationBar(CurrentPage);
if (has)
{
CurrentPage.Layout(_bounds);
_bounds = Rectangle.Zero;
}
else
{
CurrentPage.Layout(this.Bounds);
}
}
}
private Page Current
{
get { return _current; }
set
{
if (_current != null)
{
_current.PropertyChanging -= CurrentPage_PropertyChanging;
_current.PropertyChanged -= Current_PropertyChanged;
}
_current = value;
if (_current != null)
{
_current.PropertyChanging += CurrentPage_PropertyChanging;
_current.PropertyChanged += Current_PropertyChanged;
}
}
}
private Page _current;
private Rectangle _bounds;
}
And use may BaseNavigationPage instead of NavigationPage and all works fine.

MVVM Xamarin open new window from ViewModel - How to eliminate reference to the view

I want to open a new modal window using the MVVM pattern in a Xamarin Forms app. I have researched opening a new window with the MVVM pattern, which has got me this far, but the thing about windows in Xamarin forms, is they need a reference to the current page (view) to open a new window (new view) from. This forces me to pass a reference to the current page (view) from my viewModel, to my window factory, to launch the new window from. This is a violation of MVVM. My goal is to get rid of any references to views from within my viewModel. That is my question, how do I do that? My code here happens to be a modal window, but normal windows also need a reference to the page it is launching from. Here is my code and you will see what I mean:
Window Factory (look at the CreateNewWindow method):
public interface IWindowFactory
{
void CreateNewWindow();
}
public class ProductionWindowFactory: IWindowFactory
{
Page launchFromPage;
BackLogViewModel viewModel;
public ProductionWindowFactory(BackLogViewModel ViewModel, Page page)
{
viewModel = ViewModel;
launchFromPage = page;
}
public void CreateNewWindow()
{
AddStoryPage window = new AddStoryPage (new AddStoryViewModel (viewModel));
launchFromPage.Navigation.PushModalAsync (window);
}
}
}
ViewModel that opens a new modal window (look particularly at the AddTask Command):
public class BackLogViewModel : INotifyPropertyChanged
{
private IWindowFactory m_windowFactory;
public void DoOpenNewWindow()
{
m_windowFactory.CreateNewWindow();
}
public ObservableCollection<Story> AllMyStories { get; set; }
private string _updated;
public string Updated
{
get
{
return _updated;
}
set
{
_updated = value;
OnPropertyChanged ();
}
}
public Page mypage;
public BackLogViewModel (Page page)
{
Updated = DateTime.Now.ToString();
mypage = page;
AllMyStories = new ObservableCollection<Story> ();
}
public ICommand Save
{
get {
return new Command (() => {
Updated = DateTime.Now.ToString();
});
}
}
public ICommand AddTask
{
get {
return new Command ( () => {
m_windowFactory = new ProductionWindowFactory(this, mypage);
DoOpenNewWindow();
});
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs (propertyName));
}
}
private Command selectCmd;
public Command Select {
get {
this.selectCmd = this.selectCmd ?? new Command<Story>(p =>
{
var monkey = p as Story;
if (monkey == null) return;
Page z = new Views.StoryPage(p);
mypage.Navigation.PushAsync(z);
}
);
return this.selectCmd;
}
}
}
}
How do I get rid of the reference to the current page (view) within my viewModel?
I have since found This tutorial on navigating views from the ViewModel in Xamarin
It basically does what I was already doing but instead of passing the full view to the ViewModel, it passes only the INavigation interface of the view, and uses that to navigate from. It states that it can be argued that it is violating MVVM, but has the attitude of "so be it", I suspect because no obvious and easy alternatives exist. There may be alternatives that do not reference any part of the view from the ViewModel, but in order to keep moving forward I have opted for this easy solution. I have kept my window factory in order to not specify a concrete window to build in my ViewModel.

Categories

Resources