Changing heavy UserControls in WPF Window - c#

I have read a lot of subjects on the matter, on how to replace the displayed UserControl in WPF.
Suggestions tend towards Databinding of Content Control, and Kent Boggart answer to this question.
Anyways, the first problem is that my User Controls involve heavy graphical display, and are using common resources, not accessible at the same time.
What I mean by heavy graphics, on the OtherControl, there is a couple of videos plus a 3D model, reacting to dynamic events. The BallControl has an ellipse with TranslateAnimation, and a Margin reacting to events. I believe the whole project is a bit heavy due to the dynamic events rendering the pages.
So my solution was not ideal : I am clearing the content in the Grid, and replacing it with the needed User Control (the Grid takes the whole page).
The difference with the post above is that I would like (if possible) to keep my User Control as is.
The problem is that, these controls need to be switched very often, and after 1 or two hours using it, this Exception show up (line specified in the code):
System.ArgumentException : Specified Visual is already a child of another Visual or the root of a CompositionTarget class.
System.Windows.Threading.Dispatcher.Invoke(Action callback, DispatcherPriority priority, CancellationToken cancellationToken, TimeSpan timeout)
My code is obvisously wrong and I wished a cleaner solution. What's more, I've understood that there is only one UI thread, and pre-load already constructed Controls before diplaying them was not possible.
To be more precise, a bit of code is more than welcome :
MainWindow.xaml :
<Window x:Class="Testing.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Testing"
xmlns:h="http://helix-toolkit.org/wpf"
Title="MainWindow" Height="1080" Width="1920" WindowStartupLocation="CenterScreen" WindowStyle="None" MinHeight="1080" MinWidth="1920" ResizeMode="NoResize">
<Grid x:Name="Griddy">
</Grid>
MainWindow.xaml.cs :
BallControl _boule;
OtherControl _otherControl;
public MainWindow()
{
InitializeComponent();
Mouse.OverrideCursor = Cursors.None;
EventReceive.Subscribe(this);
// Instanciate both UserControls
_boule = new BallControl();
_otherControl = new OtherControl();
panelToDisplay = Turn.Ball;
Griddy.Children.Add(boule);
}
/// <summary>
/// Event coming from panels to notify which one is to be displayed
/// returning value is an Enum
/// </summary>
public void Receive<T>(T arg) where T : EventArgs
{
var casted = arg as EventArriving;
if (casted != null)
{
PanelToDisplay = casted.TurningTable;
}
}
private Turn panelToDisplay;
public Turn PanelToDisplay
{
get { return panelToDisplay; }
set
{
if (panelToDisplay != value)
{
panelToDisplay = value;
if (panelToDisplay == Turn.Other)
{
try
{
this.Griddy.Dispatcher.Invoke((Action)(() => Griddy.Children.Add(_otherControl))); //Where the exception shows up
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
try
{
this.Griddy.Dispatcher.Invoke((Action)(() => Griddy.Children.Remove(_boule)));
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
if (panelToDisplay == Turn.Ball)
{ // Bad Pattern where I remove a control to display another one.
try
{
this.Griddy.Dispatcher.Invoke((Action)(() => Griddy.Children.Add(_boule))); //Where the exception shows up
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
try
{
this.Griddy.Dispatcher.Invoke((Action)(() => Griddy.Children.Remove(_otherControl)));
}
catch (Exception e)
{
MessageBox.Show(e.ToString());
}
}
}
}
}
To summarize a little :
How do I replace/change/add & remove a User Control from the MainWindow in a clean way?
How do I make it look like it doesn't have to load back up each time it changes, even when the controls are graphically heavy?

Related

Xamarin Picker showing undesirable elements on UI

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();
}

Always get a System.NotSupportedException when quitting a view after subscribing to OnPreDrawListener

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;
}
}

Prism SetProptery not firing Notification?

So having a play with PRISM and I have a grid who's Visibility property is bound to a property in a view model like so.
Xaml
Grid Grid.RowSpan="2" Grid.ColumnSpan="3" Background="#7F808080" Visibility="{Binding LoadingVisibility}">
Code Behind
private Visibility loadingVisibility = Visibility.Visible;
public Visibility LoadingVisibility
{
get
{
return loadingVisibility;
}
set
{
SetProperty(ref loadingVisibility, value);
}
}
Now if I do this LoadingVisibility = Visibility.Collapsed;, the grid does not disappear and is still visible.
If I then set a breakpoint at SetProperty(ref loadingVisibility, value); I can see the original value of loadingVisibility, which is set as Visibile, and I can see that value is set to Collapsed.
If I then step on I can see loadingVisiblity has now changed to collapsed as it should. At this point I expect the Grid to be notified which in turn executes 'Get' to retrieve the value. This does not occur.
The binding is working because the Get is called when loading up and if I change private Visibility loadingVisibility = Visibility.Visible; to Collapsed and run the code the grid starts invisible.
So my question is, after SetProperty is executed, why is the Get not?
EDIT:
Just so you can see where I set the property.
public ShellViewModel(IEventAggregator IEventAggregator)
{
IEventAggregator.GetEvent<PubSubEvent<HardwareLoaded>>().Subscribe(x =>
{
if (!x.HardwareOK)
{
MessageBox.Show("There was an issue loading hardware. See Log");
}
LoadingVisibility = Visibility.Collapsed;
});
}
EDIT 2:
Just found something interesting, if I comment out LoadingVisibility = Visibility.Collapsed; in the Subscribe and then add a button to the xaml and have the click event like so then everything works fine.
private void Button_Click(object sender, RoutedEventArgs e)
{
mvm.LoadingVisibility = Visibility.Collapsed;
}
So now I guess the question is why, when both methods call the set property, does only one fully work and cause the Get to work?
Quite often when using the EventAggregator, you're working on the UI thread. But seeing the HardwareLoaded type it came to me that you might be doing some checking on another thread. And as we know, bindings have to be updated from the UI thread. Normally you would use Dispather.BeginInvoke, but Prism's EventAggregator has an overload in the Subscribe method to tell the handler to offload to the UI thread.
IEventAggregator.GetEvent<PubSubEvent<HardwareLoaded>>().Subscribe(x =>
{
if (!x.HardwareOK)
{
MessageBox.Show("There was an issue loading hardware. See Log");
}
LoadingVisibility = Visibility.Collapsed;
}, ThreadOption.UIThread);

WPF - Binding events to class methods of Item in ItemControl

I'm a bit new to WPF/XAML (though I've learnt C#) and would really appreciate any help for my question. I did look around other posts and google for a while but I can't seem to find a satisfactory or detailed answer to get me going on with my project. Please look below for details. Thanks you in advance!
Objective
I have a class called Tile that consists of a few properties and an event handler.
I also have an ItemControl that has a button (as by the DataTemplate), and whose ItemSource is a collection of Tiles.
Now, I want to bind the "Click" event of the Button so as to invoke the Event Handler method defined in the class Tile.
In other words when I click the button of any item in the ItemControl, the method handler of the corresponding Tile instance (from the collection) must be invoked. How would I tackle this problem?
Below is the entire code, simplified to avoid distraction:
XAML
<Window x:Class="SampleWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="300">
<!-- Make a ItemControl for "Tile"s. -->
<ItemsControl x:Name="TileList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<!-- Wire the click event of this Button
to event handler in the Tile class. -->
<Button Content="Show"></Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
CODE-BEHIND
namespace SampleWPF
{
public partial class MainWindow : Window
{
ObservableCollection<Tile> tiles;
public MainWindow()
{
InitializeComponent();
// Adding some sample data for testing.
tiles = new ObservableCollection<Tile>();
tiles.Add(new Tile("Item 1"));
tiles.Add(new Tile("Item 2"));
TileList.ItemsSource = tiles;
}
}
public class Tile : INotifyPropertyChanged
{
public string Data
{ /* Accessors and PropertyNotifiers */ }
public Tile(string data)
{ /* Initializing and assigning "Data" */ }
// INotifyPropertyChanged implementation...
// { ... }
// This event handler should be bound to the Button's "Click" event
// in the DataTemplate of the Item.
public void ShowButton_Click(object sender, EventArgs e)
{
MessageBox.Show("Viewing item from: " + this.Data);
}
}
}
Hence, if I click the first "Show" button, the output should be "Viewing item from: Item 1" and if I click the second "Show" Button, the output should be "Viewing item from: Item 2".
So what is the recommended/efficient way to do this? Is my code inappropriate for this requirement?
Event handlers are the wrong approach - use Commands and more importantly MVVM.
As I can see that you are new (and probably from a WinForms or ASP.NET background) you should read this blog to understand how your thinking needs to change - this is the most important part to understand before tackling WPF: http://rachel53461.wordpress.com/2012/10/12/switching-from-winforms-to-wpfmvvm/
You should also read Kent Boogart's blog on how MVVM works from base principles: http://kentb.blogspot.co.uk/2009/03/view-models-pocos-versus.html
Let me start with some basics:
Don't assign itemsource in codeBehind - use Binding like this:
<Controll ItemSource="{Binding MyObservableCollection}"/>
There are many ways You can achieve this. I think that using this.Data is not the best solution for this.
For example if Your tail have ID or something You can assign this id to button CommandParameter like below
<Button CommanParameter="{Binding Path=ID}" Click="ShowButton_Click"/>
And then in Your button_click event u can 'catch' this like this:
public void ShowButton_Click(object sender, EventArgs e)
{
int ID = int.Parse(((Button)sender).CommandParameter.ToString());
}
EDIT
To use this binding You need to set DataContext. You can do this in ctor like this:
public MainWindow()
{
InitializeComponent();
// Adding some sample data for testing.
tiles = new ObservableCollection<Tile>();
tiles.Add(new Tile("Item 1"));
tiles.Add(new Tile("Item 2"));
// below You are setting a datacontext of a MainWindow to itself
this.DataContext = this;
}
ANOTHER EDIT
Let's assume Your tail class have property called ID. If You bound this ID to Button.CommandParameter You can later retrieve the tile with linq like this:
public void ShowButton_click(object sender, EventArgs e)
{
int MyId = int.Parse(((Button)sender).CommandParameter.ToString());
Tile TileIWasSearchingFor = (from t in tiles where t.ID == MyId select t).First();
// do something with tile You found
}
Well since my requirement was rather "simple", I've managed a work around, avoiding commands. Thanks to the answer here by MajkeloDev: https://stackoverflow.com/a/27419974/3998255 for guidance.
This is the final event handler:
public void ShowButton_Click(object sender, EventArgs e)
{
Tile requestingTile = (sender as Button).DataContext as Tile;
if(requestingTile != null)
MessageBox.Show("Viewing item from: " + this.Data);
// Or whatever else you want to do with the object...
}
Also, adding the ItemSource as a XAML attribute:
<ItemsControl x:Name="TileList" ItemsSource="{Binding tiles}">
And setting DataContext in constructor of MainWindow:
public MainWindow()
{
this.DataContext = this;
// Whatever else you want to do...
}
Well it works as required.

InvalidOperationException, the calling thread should be STA because

I am developping a project in order to manipulate a lot of objects with several modalities (mouse, leapmotion, touch ...). I made it using the MVVM pattern soI have several Views and ViewModels for all the components I will use. To make it easier to develop I chose to have a Canvas component in which I will manipulate Grids. Each Grid can contain any type of object (Shape, Text, Image, Documents...).
To be able to have all modalities linked to my method, I decided to build one listener per modality (1 for the mouse, 1 for the leapmotion...) and make them detect basic gestures (as Click, DoubleClick ...). All the gestures I chose to detect are associate with a method via a Dictionary. Anyway the linking is working as expected as it executes the right method. T o give an example I have the action calling in my mouse listener:
if (_leftClickCounter == 1 && _capturedLeft == false)
{
if (_dic.ContainsKey(Key.OnClick))
{
Action<object> action = _dic[Key.OnClick];
action.Invoke(null);
}
}
Where:
_dic is my dictionary
Key an enumeration of gestures (as OnClick, OnDoubleClick ...)
action the method to execute
In my example the method executed is:
public void Add(object sender)
{
ObjectModel objectModel = new ObjectModel();
ObjectView objectView = new ObjectView(objectModel);
this.objectViews.Add(objectView);
}
Where sender is just used for test purpose. It remains unused in the method. My execution stops when it tries to instanciate my ObjectView saying:
InvalidOperationException
The calling thread must be STA, because many UI components require this
My ObjectView.xaml.cs class is:
public partial class ObjectView : UserControl
{
public ObjectView(ObjectModel obj)
{
InitializeComponent();
EventLinker linker = new EventLinker(this.visualBox);
ObjectViewModel objectVM = new ObjectViewModel(obj, linker);
this.DataContext = objectVM;
}
}
And its ObjectView.xaml defining the UserControl to use is very basic:
<UserControl x:Class="AusyTouchMultimodal_v1.View.ObjectView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid x:Name="visualBox" Background="Blue"/>
</UserControl>
I dont have any compilation errors, just this InvalidOperationException. Can someone explain this issue to me?
Thanks!
Try calling your actions in ui thread, like this
if (_leftClickCounter == 1 && _capturedLeft == false)
{
if (_dic.ContainsKey(Key.OnClick))
{
Action<object> action = _dic[Key.OnClick];
// action.Invoke(null);
System.Windows.Application.Current.Dispatcher.BeginInvoke( call your action )
}
}

Categories

Resources