I've got some XAML ContentPages that have ContentView objects on them. So, typically I size these objects based on this.Width and this.Height, values that are set in the ContentPage. But, if there's not something explicitly calling my ContentView objects AFTER the ContentPage has loaded, the Width and Height values are null because the values aren't yet set.
What I'm trying to figure out is, how can I tell my ContentViews to wait until the ContentPage is done loading before it gets the Width and Height values?
Xamarin.Forms provides two lifecycle-type events:
OnAppearing
OnDisappearing
which can be overridden in Page subclasses.
Now the fact, OnAppearing will be executed before the screen comes, when you want an Loaded event that needs to be executed right after the screen comes, there is a workaround.
The work around
Create an property in viewmodel like below.
private bool _toggleTemp;
public bool ToggleTemp
{
get => _toggleTemp;
set => SetProperty(ref _toggleTemp, value);
}
Add the following line to the last line of the constructor. LoadingVm.ToggleTemp = true;
Add an Switch to your screen and make IsVisible to false as below. (just for demo purposes)
<Switch IsToggled="{Binding ToggleTemp}" Toggled="Switch_OnToggled" IsVisible="False" />
Now you can write the code that you want to write in Page Loaded in Switch_OnToggled.
private async void Switch_OnToggled(object sender, ToggledEventArgs e)
{
/* Your code goes here... */
}
Please let me know if you have any doubts
I've found another workaround and it is working in Xamarin Forms 4.8:
private bool isStarted = false;
protected override void LayoutChildren(double x, double y, double width, double height)
{
base.LayoutChildren(x, y, width, height);
if (!isStarted)
{
isStarted = true;
// do something
}
}
Related
Pulling my hair out at the point. My Picker is showing an annoying line on the UI and/or the Picker's Title property if that's enabled. I simply want the Picker, not the stuff showing on the UI beneath it. Any idea on how to achieve this? Do I have to use a custom renderer or is there something simple I'm missing?
Note: The list is intentionally empty in the below examples.
Without the title, I click the Existing button, the line shows, click it again and the modal appears:
With the title, I click the Existing button, the line and title show, click it again and the modal appears:
Don't know why I have to click the button twice. But it's only on the initial page load. If I exit the modal and click the button again, it immediately appears, no double-click. Not sure if that's related to my original question, but thought I'd include it for additional information.
NewSubjectPage.xaml (chopped for brevity)
<ContentPage.Content>
<StackLayout x:Name="NewSubjectMainLay">
<ScrollView>
<StackLayout x:Name="NewSubjectChildLay">
<Grid>
<Button
x:Name="NewSubjectExisChrtBtn"
Clicked="NewSubjectExisChrtBtn_Clicked"
Grid.Column="2"
Text="Existing" />
</Grid>
</StackLayout>
</ScrollView>
<Picker
x:Name="NewSubjectExisChrtPck"
IsVisible="False"
ItemsSource="{Binding Charts}"
ItemDisplayBinding="{Binding Name}"
SelectedIndexChanged="NewSubjectExisChrtPck_SelectedIndexChanged"
Title="Select chart"
Unfocused="NewSubjectExisChrtPck_Unfocused"/>
</StackLayout>
</ContentPage.Content>
NewSubjectPage.xaml.cs (chopped for brevity)
public partial class NewSubjectPage : ContentPage
{
private string chartName;
private readonly NewSubjectViewModel _viewModel;
public string ChartName
{
get => chartName;
private set
{
chartName = value;
OnPropertyChanged();
}
}
public NewSubjectPage()
{
InitializeComponent();
BindingContext = _viewModel = new NewSubjectViewModel();
chartName = "";
}
private void NewSubjectExisChrtBtn_Clicked(object sender, EventArgs e)
{
_viewModel.LoadChartsCommand.Execute(null);
NewSubjectExisChrtPck.IsVisible = true;
NewSubjectExisChrtPck.Focus();
}
private void NewSubjectExisChrtPck_SelectedIndexChanged(object sender, EventArgs e)
{
var picker = (Picker)sender;
int selectedIndex = picker.SelectedIndex;
if (selectedIndex != -1)
{
ChartName = picker.Items[picker.SelectedIndex];
}
}
private void NewSubjectExisChrtPck_Unfocused(object sender, FocusEventArgs e)
{
NewSubjectExisChrtPck.IsVisible = false;
NewSubjectExisChrtPck.Unfocus();
}
}
NewSubjectViewModel.cs (chopped for brevity)
class NewSubjectViewModel : BaseViewModel
{
private ObservableCollection<Chart> charts;
public ObservableCollection<Chart> Charts
{
get { return charts; }
private set
{
charts = value;
OnPropertyChanged();
}
}
public Command LoadChartsCommand { get; set; }
public NewSubjectViewModel()
{
LoadChartsCommand =
new Command(
async () => await ExecuteLoadChartsCommand()
);
}
private async Task ExecuteLoadChartsCommand()
{
try
{
IndicatorRunning = true;
var list = await App.Database.GetChartsAsync();
Charts = new ObservableCollection<Chart>(list);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
}
Thanks for your help! Let me know if you need to see anything else.
First, I was not able to reproduce the issue of the modal not showing until a second click of the button. You might need to provide more code for that to happen. To even use your code sample I had to replace var list = await App.Database.GetChartsAsync(); with something else to simulate a long running task that returns an empty list. Also had to create a Chart type with a Name property. Not to mention BaseViewModel. In the future, please provide all code to reproduce the issue so there is minimal work required of the person who is trying to help you. There is concept on Stack Overflow called the MCVE (minimal, complete, verifiable example): http://stackoverflow.com/help/mcve
That said, perhaps the first click is actually focusing the emulator and making it the active app, and then the second is the first actual click on the button? This I can reproduce. IOW, if the emulator is not the foreground app, you have to click it once to make it active and then your app will handle clicks.
As for the undesirable UI, you do realize that the Picker UI is basically a clickable label that when clicked displays the actual picker modal? So when you make it visible, what you are making visible is the label UI, which has the line and the Title (if set), and when you focus that label, then the actual picker dialog is displayed. If you don't want to see the UI Label at all, then why make it visible? You can focus it without making it visible, so just remove the line NewSubjectExisChrtPck.IsVisible = true;
As a side note, when you call _viewModel.LoadChartsCommand.Execute(null); that calls an async method, var list = await App.Database.GetChartsAsync(); , so the LoadChartsCommand returns before you set the Charts property, and also then the code following the call to _viewModel.LoadChartsCommand.Execute(null); also executes before LoadChartsCommand really finishes, so you are making the picker visible and focusing it before the LoadChartsCommand finishes as well, so if you were loading actual items for the picker to display, they may not be there the first time. Maybe it's just the sample code, but I see no reason to use a command here, but rather you should just call an awaitable task. You are not binding to the LoadChartsCommand, so I see no reason for you to even use a Command in this scenario. Instead I suggest making ExecuteLoadChartsCommand public and calling it directly, e.g.:
private async void NewSubjectExisChrtBtn_Clicked(object sender, EventArgs e)
{
//_viewModel.LoadChartsCommand.Execute(null); // Returns immediately, so picker not loaded with items yet.
await _viewModel.ExecuteLoadChartsCommand(); // Waits for method to finish before before presenting the picker.
//NewSubjectExisChrtPck.IsVisible = true;
NewSubjectExisChrtPck.Focus();
}
I was wondering if it was possible to edit the Xamarin.Forms source code and then use the edited one like you normally would in your xamarin.forms project.
Basically, my goal would be to change the PhoneMasterDetailRenderer in order to change the width value of the Master page. (It is a percentage of the screen, which is 0.8, and so by changing that it should adjust the size of the master?)
Here is the section of code I wish to change:
void LayoutChildren(bool animated)
{
var frame = Element.Bounds.ToRectangleF();
var masterFrame = frame;
masterFrame.Width = (int)(Math.Min(masterFrame.Width, masterFrame.Height) * 0.8);
...
}
The issue of not being able to change the width of the master has been a problem for a very long time, and hopefully this may lead to a solution.
Thanks, Daniel.
I don't recommend you to edit the source code. But we can also create our own MasterDetailPage's Renderer. It may be a little difficult, let's do this step by step.
Firstly, define a BindableProperty in our own MasterDetailPage class like:
public readonly static BindableProperty WidthRatioProperty =
BindableProperty.Create("WidthRatio",
typeof(float),
typeof(MyMasterDetailPage),
(float)0.2);
public float WidthRatio
{
get
{
return (float)GetValue(WidthRatioProperty);
}
set
{
SetValue(WidthRatioProperty, value);
}
}
Secondly, try to create our own renderer instead of using the form's default renderer. I post my source code here about my own renderer. In this class I use widthRatio changing the master's width. This property can be set in:
void HandlePropertyChanged(object sender, PropertyChangedEventArgs e)
{
...
else if(e.PropertyName == "WidthRatio")
{
widthRatio = ((MyMasterDetailPage)Element).WidthRatio;
}
}
At last, create the custom renderer inheriting the renderer above like:
[assembly: ExportRenderer(typeof(MyMasterDetailPage), typeof(MyMasterDetailPageRenderer))]
namespace MasterDetailDemo.iOS
{
public class MyMasterDetailPageRenderer : MyPhoneMasterDetailRenderer
{
}
}
You can set the property WidthRatio's value in forms's MasterDetailPage to change the width now. You can run my demo to test it.
Besides if you want to do this on Android, please refer to this thread.
I'm in the process of developing an app to display booking information via a client's API in Xamarin.Forms. They wanted a section to display several blocks of information, and that information is often styled with HTML tags (we have no control over how this data is returned). As a quick way to throw a view together to display this information, I created a view which consists of a series of individual webviews to display this information inside a scrollview.
To make this page look less horrendous, I have made a custom renderer for the webview that, when content has been loaded, will resize the view to encapsulate all of the content without scrolling.
Now the actual problem:
To achieve this, inside the custom renderer, in the OnAttachedToWindow() callback, get the webview's ViewTreeObserver and call the AddOnPreDrawListener() method to make use of the OnPreDraw() callback;
The resize is performed in that callback, if the ContentHeight is greater than 0 then it resized the page and returns true.
Then in the OnDetachedFromWindow() callback, I check that the VTO is alive, and unsubscribe from the OnPreDraw listener.
Simple, Right?
The page works exactly as expected when navigating to it, however when trying to navigate away, It ALWAYS throws this error:
System.NotSupportedException: Unable to activate instance of type ExtendedWebViewRenderer from native handle 0xbe9f6eac (key_handle 0xf883b13).
The error does not occur if I remove all the ViewTreeObserver event subscription stuff, but if I do that, then the page will not resize accordingly.
Could someone please point me in the right direction? I have included the brief source code for this. I would appreciate any help on this:
Source
public class ExtendedWebViewRenderer : WebViewRenderer, ViewTreeObserver.IOnPreDrawListener
{
ExtendedWebView _xwebView = null;
WebView _webView;
protected override void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged (e);
_xwebView = e.NewElement as ExtendedWebView;
_webView = Control;
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
// Here assign a ViewTreeObserver to monitor when the view's children need to be resized.
var _vto = _webView.ViewTreeObserver;
_vto.AddOnPreDrawListener(this);
}
protected override void OnDetachedFromWindow()
{
var _vto = _webView.ViewTreeObserver;
if (_vto.IsAlive)
{
_vto.RemoveOnPreDrawListener(this);
}
this.Control.ClearCache(true);
this.Control.ClearAnimation();
this.Control.StopLoading();
base.OnDetachedFromWindow();
}
public bool OnPreDraw()
{
if (_webView != null)
{
// We get the view's content height and apply that to the parent view.
int contentHeight = _webView.ContentHeight;
if (contentHeight != 0 && contentHeight != _xwebView.HeightRequest)
{
int desiredHeight = contentHeight + _webView.PaddingBottom + _webView.PaddingTop;
_xwebView.HeightRequest = desiredHeight;
}
}
return true;
}
}
I've been working on developing a custom control which will be used in our CRM frontend. The control itself is nothing special, it simply wraps two labels, text edits, and a button into a single control. (The control is only acting as a wrapper, a bit long winded, but unfortunately our only option due to various restrictions)
I though it would be nice to give the control a Font and ForeColor property, that would change the Font and Color of the labels. Changing the font size means that the relative position of the text boxes be changed to keep everything in line. No problem.
I encapsulated the layout logic in an UpdateLayout method, which is called on the set accessor of the Font property and everything works beautifully at design time, however, at runtime, the Font of the labels is correct, but the layout of the text boxes and button are still in the default positions, hence, the labels overlap.
What am I missing in for updating the position of controls at the init stage in runtime? I've tried calling the UpdateLayout() method from both Initialize and the constructor of the control, alas to no avail.
Am i missing something obvious here?
EDIT:
As requested, I whipped up a quick test. My test control looks like so (Not including Designer code):
public partial class TestControl : UserControl
{
private Font _font;
[Browsable(true)]
public override Font Font
{
get
{
return this._font ?? SystemFonts.DefaultFont;
}
set
{
this._font = value;
this.DoLayout();
}
}
private void DoLayout()
{
this.label1.Font = this._font;
this.Size = new Size(label1.Width + textBox1.Width + 10,
label1.Height >= textBox1.Height ? label1.Height : textBox1.Height);
this.textBox1.Location = new Point(label1.Location.X + 5 + label1.Width, 1);
this.Update();
}
public TestControl()
{
InitializeComponent();
}
protected override void OnLayout(LayoutEventArgs e)
{
base.OnLayout(e);
DoLayout();
}
}
That works great at design time, but runtime, less so...
EDIT2:
So the above code doesn't entirely reflect the problem accurately, however, I have tried Jogy's suggestion of overriding the OnLayout method, and lo and behold, it works!
I'm relatively new to Custom Controls, so a rookie mistake on my part. This will definitely be committed to the long term memory.
Override OnLayout() method and call your UpdateLayout() there.
Thanks for supplying the code, I would provide the properties by reusing already available controls.
public override Font Font
{
get { return this.label1.Font; }
set
{
this.label1.Font = value;
// Additional code to update related controls.
}
}
Also be aware that the declaration of
private Font _font;
Delivers a non-initialized variable, and by using it in the "Do_Layout" might use a null value. Maybe change it to the following when using your code.
this.label1.Font = this.Font;
I have the following code
<Window x:Class="Netspot.DigitalSignage.Client.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" WindowStyle="SingleBorderWindow"
WindowStartupLocation="CenterScreen"
WindowState="Normal" Closing="Window_Closing">
Any attempt to get the height / width return NaN or 0.0
Can anyone tell me a way of getting it ?
These 2 methods don't work
//Method1
var h = ((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualHeight;
var w = ((System.Windows.Controls.Panel)Application.Current.MainWindow.Content).ActualWidth;
//Method2
double dWidth = -1;
double dHeight = -1;
FrameworkElement pnlClient = this.Content as FrameworkElement;
if (pnlClient != null)
{
dWidth = pnlClient.ActualWidth;
dHeight = pnlClient.ActualWidth;
}
The application will not be running full screen.
1.) Subscribe to the window re size event in the code behind:
this.SizeChanged += OnWindowSizeChanged;
2.) Use the SizeChangedEventArgs object 'e' to get the sizes you need:
protected void OnWindowSizeChanged(object sender, SizeChangedEventArgs e)
{
double newWindowHeight = e.NewSize.Height;
double newWindowWidth = e.NewSize.Width;
double prevWindowHeight = e.PreviousSize.Height;
double prevWindowWidth = e.PreviousSize.Width;
}
Keep in mind this is very general case, you MAY (you may not either) have to do some checking to make sure you have size values of 0.
I used this to resize a list box dynamically as the main window changes. Essentially all I wanted was this control's height to change the same amount the window's height changes so its parent 'panel' looks consistent throughout any window changes.
Here is the code for that, more specific example:
NOTE I have a private instance integer called 'resizeMode' that is set to 0 in the constructor the Window code behind.
Here is the OnWindowSizeChanged event handler:
protected void OnWindowSizeChanged (object sender, SizeChangedEventArgs e)
{
if (e.PreviousSize.Height != 0)
{
if (e.HeightChanged)
{
double heightChange = e.NewSize.Height - e.PreviousSize.Height;
if (lbxUninspectedPrints.Height + heightChange > 0)
{
lbxUninspectedPrints.Height = lbxUninspectedPrints.Height + heightChange;
}
}
}
prevHeight = e.PreviousSize.Height;
}
You can get the width and height that the window was meant to be in the constructor after InitializeComponent has been run, they won't return NaN then, the actual height and width will have to wait until the window has been displayed.
When WindowState == Normal You can do this one from Width / Height after IntializeComponent().
When WindowState == Maximized You could get the screen resolution for this one with
System.Windows.SystemParameters.PrimaryScreenHeight;
System.Windows.SystemParameters.PrimaryScreenWidth;
You have to try to get the ActualWidth/ActualHeight values once the window is Loaded in the UI. Doing it in Window_Loaded works well.
XAML
<Grid x:Name="Grid1">
<Image x:Name="Back_Image" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</Grid>
CS
MainWindow() after InitializeComponent();
Back_Image.Width = Grid1.Width;
Back_Image.Height = Grid1.Height;
WPF does the creation of controls and windows in a deferred manner. So until the window is displayed for the first time, it might not have gone through layouting yet, thus no ActualWidth/ActualHeight. You can wait until the window is loaded and get the properties then, or yet better bind these properties to a target where you need them. You could also force the layouting via UpdateLayout().
Just want to add: try to minimize the amount of size dependant logic, it is almost always possible to avoid it. Unless you are writing a layout panel of course.
Notice that when you use controls with 100% of with, they have a NaN size till they are being represented
You can check the ActualHeigh or ActualWidth just when the Loaded event is fired, but never try to verify it before the windows controls are created. Forget the idea to control that in the constructor.
In my oppinion, the best place to control this kind of things is The SizeChanged event
Hi you can get the height of the current screen in WPF with
System.Windows.SystemParameters.PrimaryScreenHeight
I just add a variable name to my window and then get the width or height of the window i want from its width or height property respectifly.
XAML:
<Window x:Name="exampleName"
...
</Window>
C#:
exampleName.Height;
or:
exampleName.Width;
My solution in case of MVVM...
I have a ViewModelBase, which is the base class for all ViewModel of the app.
1.) I created in the ViewModelBase a virtual method, which could be overriten in the child ViewModels.
public virtual void OnWindowSizeChanged(object sender, SizeChangedEventArgs e) {}
2.) The ctr of the MainWindow : Window class
public MainWindow(object dataContext)
{
InitializeComponent();
if (dataContext is ViewModelBase)
{
this.SizeChanged += ((ViewModelBase)dataContext).OnWindowSizeChanged;
DataContext = dataContext;
}
}
3.) Eg. in the MainViewModel, which is extends the ViewModelBase
public override void OnWindowSizeChanged(object sender, SizeChangedEventArgs e)
{
Console.WriteLine(e.NewSize.Height);
}