A little background information
I am learning Xamarin.Forms and am currently struggling a bit with dynamically coupling my ContentPage's XAML with my code-behind. Obviously, I am at the mercy of my complete unawareness of how Xamarin.Form's should be written, so I hope you can bare with my slight confusion.
I am developing a mobile application for Android and am using the BottomNavigationBarXF to put the navigation bar at the bottom, which is working well. Currently, I am using the example project for my learning.
The actual problem
I have created a series of ContentPage's which I would like to dynamically couple upon instantiating each new page. My ContentPage's have corresponding code-behind, which I have left untouched; e.g., I have a ContentPage named HomePage, which have this code-behind:
namespace BottomBarXFExample
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class HomePage : ContentPage
{
public HomePage()
{
InitializeComponent();
}
}
}
and this corresponding XAML:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="BottomBarXFExample.HomePage">
<ContentPage.Content>
<StackLayout>
<Label Text="Welcome to Xamarin.Forms!"
VerticalOptions="CenterAndExpand"
HorizontalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage.Content>
The way I go about creating pages is as follows.
string[] tabTitles = { "Me", "Trends", "Home", "Plan", "About" };
ContentPage[] pages = new ContentPage[tabTitles.Length];
for (int i = 0; i < tabTitles.Length; ++i)
{
ContentPage page = createPage(tabTitles[i]);
bottomBarPage.Children.Add(page);
}
The createPage method:
private ContentPage createPage(String title)
{
FileImageSource icon = setIcon(title);
ContentPage page = new ContentPage()
{
Title = title,
Icon = icon,
};
// should something happen here with the XAML?
return page;
}
And the setIcon method:
private FileImageSource setIcon(String title)
{
FileImageSource icon = (FileImageSource)FileImageSource.FromFile(
string.Format(
"ic_" + title.ToLowerInvariant() + ".png",
title.ToLowerInvariant()
));
return icon;
}
Using this approach I am successful in creating the bottom navigation bar. However, navigating to each page using the navigation bar, the view is "obviously" empty, because I am not linking the ContentPage to its corresponding XAML. Can this be done in code?
If I opt for instantiating each ContentPage the "right" way:
HomePage homePage = new HomePage()
{
Title = "Home",
Icon = homeIcon
};
And then add them to the navigation bar like so:
bottomBarPage.Children.Add(homePage)
I do obtain coupling between XAML and code-behind. However, I find it rather tedious, and probably also unnecessary, to do it this way.
Any suggestions?
Thank you,
Kris
Xaml pages and code behind classes are tightly coupled in xaml file with x:Class definition. Xaml pages cannot be inherited, but ContentPage classes can but I don't see that resolving your issues. If you're looking after having only one xaml page then you'd have to create the rendering logic in code behind, e.g.
public HomePage(string title)
{
InitializeComponent();
switch(title)
{
// set Binding Context to your VM
... BindingContext = titleBasedVM;
}
}
Your VM can then contain page specific data. This concept uses MVVM, which is highly recommended when using Xamarin Forms.
Also take a look at ControlTemplate for rendering generic page sections.
You shouldn't try to generate xaml dynamically as it's not supported.
Related
I use the community tool MVVM for my current MAUI project.
I would like to dynamically add controls like an entry to the GUI during runtime. I would like to do that from the ViewModel.
Using the toolkit, it is of course very easy to provide and interact with functions and properties. Unfortunately I haven't found a way to directly access a StackLayout or something similar.
I tried giving the VerticalStackLayout property x:name (in my xaml document) a name and then accessing it. This works from the code-behind, but not from the ViewModel itself.
I expected that with in the viewModel for example my StackLayout is displayed and then I can execute the following.
stackLayout.Add(new Label { Text = "Primary colors" }));
Furthermore I tried to provide a binding to the property x:name.
x:Name="{Binding StackLayout}
In the ViewModel I then tried to provide the property.
[ObservableProperty]
VerticalStackLayout stackLayout;
To clarify: the ViewModel doesn't know about the View, but the View DOES know about the ViewModel.
Thus, the view's code behind can do what is needed.
If the View doesn't already have a property holding the viewmodel, then add to code behind:
private MyVM VM => (MyVM)BindingContext;
That defines a VM property, so you can do VM.MyDictionary[someKey] or similar.
If you need to access VM in constructor BEFORE setting BindingContext,
then edit question, to show how BindingContext is set currently.
Yes, you can use MVVM to achieve this.
A simple method is to use Bindable Layouts to achieve this.
Please refer to the following code:
1.create a viewmodel for current page
MyViewModel.cs
public class MyViewModel
{
public int index = 0;
public ObservableCollection<Data> Items { get; set; }
public ICommand AddItemCommand => new Command(addItemMethod);
private void addItemMethod(object obj)
{
index++;
Items.Add(new Data { FileName ="File " + index});
}
public MyViewModel()
{
Items = new ObservableCollection<Data>();
}
}
Data.cs
public class Data
{
public string FileName { get; set; }
}
2.MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:mauiapp="clr-namespace:MauiAddViewApp116"
x:Class="MauiAddViewApp116.MainPage"
x:Name="mainpage"
>
<ContentPage.BindingContext>
<mauiapp:MyViewModel></mauiapp:MyViewModel>
</ContentPage.BindingContext>
<ScrollView>
<VerticalStackLayout
Margin="10"
VerticalOptions="StartAndExpand">
<Button Text="Add item" Command="{Binding AddItemCommand}"></Button>
<StackLayout BindableLayout.ItemsSource="{Binding Items}" Orientation="Vertical">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Label HorizontalOptions="Fill" Text="{Binding FileName}" FontSize="Large" HeightRequest="38" />
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</VerticalStackLayout>
</ScrollView>
</ContentPage>
I have found a solution to my problem.
As you have advised me, I have put it around. I use the code-behind of my view to access the StackLayout.
1. MainPage.xaml
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center"
x:Name="VStackLayout">
</VerticalStackLayout>
</ScrollView>
With the property x:name I can access the VS layout from the code behind.
2. MainPage.xaml.cs
Dictionary<string, object> keyValuePairs = new Dictionary<string, object>();
public MainPage(MainPageViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
foreach (var item in viewModel.KeyValues)
{
if (item.Value == "String")
{
keyValuePairs.Add(item.Key, "");
var entry = new Entry {
Placeholder = item.Key,
ClassId = item.Key,
Text = (String)keyValuePairs.Where(k => k.Key == item.Key).First().Value
};
VStackLayout.Add(entry);
}
else if (item.Value == "Boolean")
{
keyValuePairs.Add(item.Key, true);
Label label = new Label { Text = item.Key};
var toogle = new Switch
{
IsEnabled = true,
ClassId = item.Key,
IsToggled = (Boolean)keyValuePairs.Where(k => k.Key == item.Key).First().Value
};
HorizontalStackLayout views = new HorizontalStackLayout();
views.HorizontalOptions = LayoutOptions.StartAndExpand;
views.VerticalOptions = LayoutOptions.Center;
views.Add(label);
views.Add(toogle);
VStackLayout.Add(views);
}
}
Here the Dic in the ViewModel is accessed and then the GUI is created from it.
Unfortunately, the access to the content of the elements (entries) does not work yet. I would like to see how to write the content in a Dictonary. The binding at this point does not work yet. Does anyone have an idea?
First of all, I want to answer that nothing is stopping you from passing a reference of your StackLayout as CommandParameter to your Command in the ViewModel. Write this:
[RelayCommand]
void Add(StackLayout myLayout)...
And just pass the reference in the XAML.
However, there are very few situations that justify this.
None of those situations are "to customize the GUI".
You need to learn how to use DataTemplates, DataTriggers, Styles, EventToCommandBehaviors, Gestures, ControlTemplates, Validators and ValueConvertors.
This will cover your basic needs for accessing the View and its elements.
I have a loading overlay (with the View inheriting from UserControl and the ViewModel from ViewModelBase) that I display over the current window by putting using a <Grid> and having the regular controls in a <StackPanel> and then the loading screen after it in a <Border>, binding the <Border>'s IsVisible property to control the display of the overlay.
<Window ...>
<Grid>
<StackPanel>
<!-- Window controls here -->
</StackPanel>
<Border Background="#40000000"
IsVisible="{Binding IsLoading}">
<views:LoadingScreenView />
</Border>
</Grid>
</Window>
In the LoadingScreenViewModel I use an HttpClient to download a JSON file to parse and display on the loading overlay.
It is refreshed in the LoadingScreenViewModel every 10 seconds by using a timer
private IObservable<long> timer = Observable.Interval(TimeSpan.FromSeconds(10),
Scheduler.Default);
and then subscribing to it in the ViewModel's constructor
public LoadingScreenViewModel()
{
LoadingText = "Loading...";
timer.Subscribe(async _ =>
{
var json = await _httpClient.GetStringAsync(...);
var dict = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
LoadingText = dict["result"];
});
}
The problem is that since I include the LoadingScreenView in the window, this timer is firing every ten second, even when the loading overlay isn't displayed.
Is there any way to check if the overlay itself is visible during that Subscribe(), passing the IsLoading property to the LoadingScreenViewModel, or creating and destroying the View every time it is used?
I was able to achieve this by adding a bool isVisible property to the LoadingScreenViewModel, having the view inherit from ReactiveUserControl<LoadingScreenViewModel>, and per the discussion at https://github.com/AvaloniaUI/Avalonia/discussions/7876 I achieved this in code-behind by subscribing to changes in the view's TransformedBounds property and determining if the view is visible based on if TransformedBounds is null or not.
public LoadingScreenView()
{
InitializeComponent();
this.WhenAnyValue(v => v.TransformedBounds)
.Subscribe(x => ViewModel.isVisible = x is not null);
}
I have an AvalonDock (version 3.5) in my MVVM WPF application. The binding is as follows:
<xcad:DockingManager Name="_dockingManager" Grid.Row="1"
DataContext="{Binding DockingManagerViewModel}"
DocumentsSource="{Binding Documents}"
AnchorablesSource="{Binding Anchorables}" >
So when I add a new Anchorable to the corresponding "Anchorables" collection in my view model the corresponding view shows up. Nevertheless the views always show up docked on the right side of my application. How can I control that the view is docked on the left side of my application via the view model?
I don't think you can control this in you viewmodel.
There 2 ways for controlling this.
You can restore the layout from a previously saved (default) layout whenever the
application is started for te first time or
You can setup the XAML to use an initial layout as you wish (prefered solution)
For the second option:
You can use the XAML binding in the DockingManager class to implement your requirement:
See TestApp sample for full implementation of the below snippet (just change LeftSide to BottomSide to see the effect):
<avalonDock:DockingManager Grid.Row="1">
...
<avalonDock:LayoutRoot.LeftSide>
<avalonDock:LayoutAnchorSide>
<avalonDock:LayoutAnchorGroup>
<avalonDock:LayoutAnchorable Title="AutoHide1 Content" ContentId="AutoHide1Content" IconSource="/AvalonDock.TestApp;component/Images/address-book--pencil.png" >
<TextBox Text="{Binding TestTimer, Mode=OneWay, StringFormat='AutoHide Attached to Timer ->\{0\}'}"/>
</avalonDock:LayoutAnchorable>
<avalonDock:LayoutAnchorable Title="AutoHide2 Content" ContentId="AutoHide2Content">
<StackPanel Orientation="Vertical">
<TextBox/>
<TextBox/>
</StackPanel>
</avalonDock:LayoutAnchorable>
</avalonDock:LayoutAnchorGroup>
</avalonDock:LayoutAnchorSide>
</avalonDock:LayoutRoot.LeftSide>
</avalonDock:LayoutRoot>
</avalonDock:DockingManager>
You can add a property (call it InitialPosition, or something like that) to your Anchorable view model, and implement a ILayoutUpdateStrategy to position the anchorable on the left, right or bottom side.
Add something like this to your XAML:
<xcad:DockingManager …>
…
<xcad:DockingManager.LayoutUpdateStrategy>
<local:LayoutUpdate />
</xcad:DockingManager.LayoutUpdateStrategy>
</xcad:DockingManager>
and your LayoutUpdate class:
class LayoutUpdate: ILayoutUpdateStrategy
{
static Dictionary<PaneLocation, string> _paneNames = new Dictionary<PaneLocation, string>
{
{ PaneLocation.Left, "LeftPane" },
{ PaneLocation.Right, "RightPane" },
{ PaneLocation.Bottom, "BottomPane" },
};
public bool BeforeInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorableToShow, ILayoutContainer destinationContainer)
{
if (anchorableToShow.Content is IAnchorable anch)
{
var initLocation = anch.InitialLocation;
string paneName = _paneNames[initLocation];
var anchPane = layout.Descendents()
.OfType<LayoutAnchorablePane>()
.FirstOrDefault(d => d.Name == paneName);
if (anchPane == null)
{
anchPane = CreateAnchorablePane(layout, Orientation.Horizontal, initLocation);
}
anchPane.Children.Add(anchorableToShow);
return true;
}
return false;
}
static LayoutAnchorablePane CreateAnchorablePane(LayoutRoot layout, Orientation orientation,
PaneLocation initLocation)
{
var parent = layout.Descendents().OfType<LayoutPanel>().First(d => d.Orientation == orientation);
string paneName = _paneNames[initLocation];
var toolsPane = new LayoutAnchorablePane { Name = paneName };
if (initLocation == PaneLocation.Left)
parent.InsertChildAt(0, toolsPane);
else
parent.Children.Add(toolsPane);
return toolsPane;
}
public void AfterInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorable)
{
// here set the initial dimensions (DockWidth or DockHeight, depending on location) of your anchorable
}
This code is extracted and changed a bit from the working application, with different types and names. It should probably work, but there might be a typo or other error somewhere.
I read many similar posts here but I still have some questions regarding not only how to accomplish this but if there is a better or more appropriate way to accomplish this. This being that I have a WPF application in which I have a Main window that instantiates a page object called ScratchPad that contains a textbox and a method to update the contents of that textbox.
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
ScratchPad scratchPad = new ScratchPad();
}
}
Here's the associated XAML that also instantiates(?) my other applications in frames within the MainWindow.
<DockPanel>
<TabControl TabStripPlacement="Left">
<TabItem Header="Main">
<Frame Source="Common/GUI/ScratchPad.xaml" ></Frame>
</TabItem>
<TabItem Header="Test Apps">
<Frame Source="Apps/TestApp/View/authPrompt_View.xaml" Margin="0,0,0,191.2" />
</TabItem>
<TabItem Header="Threads">
</TabItem>
</TabControl>
</DockPanel>
This object is intended to display log materials to report on the status of operations the application performs. The code for ScratchPad can be seen below.
public partial class ScratchPad : Page
{
public ScratchPad()
{
InitializeComponent();
}
public void updateStatus(string newText)
{
scratchPadTextBox.AppendText(newText);
}
}
My intention is to have many of my other classes be able to append to that textbox, however I believe in order to do this I would need to pass a reference to the MainWindow object to each of the classes that want to write to that textbox. My problem is that the other classes are not directly instantiated (to my knowledge) and as a result I'm not quite sure how to accomplish this or even if this is how it should be done. Here's a sample of a class that I would like to be able to append to the textbox which is created when the user hits submit on a page that is instantiated(?) through a frame source in the MainWindow's XAML.
class ConnectionManager
{
public void authenticateSharePoint(string urlAddress)
{
DataContextRef.DataContextRefDataContext dc =
new DataContextRef.DataContextRefDataContext(new Uri("redacted.svc"));
dc.Credentials = System.Net.CredentialCache.DefaultNetworkCredentials; var source = dc.Test;
((System.Net.NetworkCredential)dc.Credentials).Domain = "blah";
((System.Net.NetworkCredential)dc.Credentials).UserName = "blah";
((System.Net.NetworkCredential)dc.Credentials).Password = "hardcodeisthebest123";
foreach (var item in source)
{
scratchPad.updateStatus("item.name: " + item.Name);
updateStatus("item.title: " + item.Title);
updateStatus("item.path: " + item.Path);
updateStatus("item.id: " + item.Id);
}
}
As seen in the XAML above copied again below, this class is created when a user selects the submit button in a separate class that is hosted in a frame in the MainWindow
<TabItem Header="Test App">
<Frame Source="Apps/TestApp/View/authPrompt_View.xaml" Margin="0,0,0,191.2" />
</TabItem>
As is always the case, when I type out my issue I realize just how many areas I need to address from a knowledge gap perspective. Any insight and/or assistance is appreciated!
however I believe in order to do this I would need to pass a reference to the MainWindow object to each of the classes that want to write to that textbox.
You could get a reference to the existing instance of the MainWindow using the Application.Current.Windows collection:
MainWindow mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
if(mainWindow != null)
{
//...
}
To be able to access your main view's object you have to create an instance of it from the class you want to access it from.
ScratchPad name = new ScratchPad();
Now you should be able to access the scratch pad with something like :
name.updateStatus.scratchPadTextBox
I need to write a small application to read a configuration file and generate some report with it. I was hoping to finally use MVVM but it's quite tricky to get started. Oh, I'm using Caliburn.Micro framework.
So this is what I have, a shell (primary view that hosts other views) that has a ribbon with 3 buttons on it:
1) Open file
2) Show settings
3) Show results
And two other views, SettingsView and ResultsView with buttons to generate and delete a report.
So I guess the view structure would be like this:
ShellView
Ribbon
OpenFileButton
SettingsButton
ResultsButton
ContentControl (hosts SettingsView and ResultsView)
SettingsView
CalculateResultsButton
ResultsView
CancelResultsButton
The tricky part is this:
1. "Show settings" button is disabled until a file is opened (via Open file).
2. "Show results" button is disabled until a report is calculated (via a
method in SettingsViewModel).
3. If a report is calculated, the CalculateResultsButton is disabled and
CancelResultsButton is enabled and vice versa.
Please advise how could I achieve this ? I've no ideas what strategy should I go for. My non-MVVM-thinking-brain says that I should create a status variable and then somehow bind those buttons to that variable, but I guess that wont work in a MVVM world, right ? Any code example would be very very very appreciated!
Many thanks!
Since you're using CM you won't need any code-behind. You can delete the .xaml.cs files if you want.
This is a pretty basic example but it should give you an idea on how to control the state of the buttons. In this example, Open will be enabled and the other two are disabled. If you click on Open, Settings is enabled. The same happens with Results once Settings is clicked.
If you need a way to do global state the same concept can be applied by injecting a singleton, SharedViewModel, into the ViewModels and the CanXXX methods can check values in SharedViewModel. This is a SL demo of different things but one is injecting a singleton to share data, the same idea applies in wpf.
ShellView:
<Window x:Class="CMWPFGuardSample.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0"
Orientation="Horizontal">
<Button x:Name="Open"
Content="Open" />
<Button x:Name="Settings"
Content="Settings" />
<Button x:Name="Results"
Content="Results" />
</StackPanel>
</Grid>
</Window>
ShellViewModel:
[Export(typeof (IShell))]
public class ShellViewModel : PropertyChangedBase, IShell
{
private bool _isOpen;
public bool IsOpen
{
get { return _isOpen; }
set
{
_isOpen = value;
NotifyOfPropertyChange(() => IsOpen);
NotifyOfPropertyChange(() => CanSettings);
}
}
private bool _isSettings;
public bool IsSettings
{
get { return _isSettings; }
set
{
_isSettings = value;
NotifyOfPropertyChange(() => IsSettings);
NotifyOfPropertyChange(() => CanResults);
}
}
public bool IsResults { get; set; }
public void Open()
{
IsOpen = true;
}
public bool CanSettings
{
get { return IsOpen; }
}
public void Settings()
{
IsSettings = true;
}
public bool CanResults
{
get { return IsSettings; }
}
public void Results()
{
}
}
MVVM and WPF Commands perfectly fits your "tricky part" requirements since have built in ICommand.CanExecute() method which allows enabling/disabling corresponding button based on custom logic.
To use this naice feature take a look first at the RoutedCommand Class and self explanatory example on MSDN How to: Enable a Command (see below code snippets).
And in general about MVVM, it is really SIMPLE! Just try it and you won't leave without it ;) In few words - you have to create for each EntityView.xaml corresponding EntityViewModel class and then just put instance of it in the View's DataContext either explicitly in code or using bindings:
var entityViewModel = new EntityViewModel();
var view = new EntityView();
view.DataContext = entityViewModel;
MVVM Command and Command.CanExecute bindings:
XAML:
<Window x:Class="WCSamples.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="CloseCommand"
Name="RootWindow"
>
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Close"
Executed="CloseCommandHandler"
CanExecute="CanExecuteHandler"
/>
</Window.CommandBindings>
<StackPanel Name="MainStackPanel">
<Button Command="ApplicationCommands.Close"
Content="Close File" />
</StackPanel>
</Window>
C# code behind:
// Create ui elements.
StackPanel CloseCmdStackPanel = new StackPanel();
Button CloseCmdButton = new Button();
CloseCmdStackPanel.Children.Add(CloseCmdButton);
// Set Button's properties.
CloseCmdButton.Content = "Close File";
CloseCmdButton.Command = ApplicationCommands.Close;
// Create the CommandBinding.
CommandBinding CloseCommandBinding = new CommandBinding(
ApplicationCommands.Close, CloseCommandHandler, CanExecuteHandler);
// Add the CommandBinding to the root Window.
RootWindow.CommandBindings.Add(CloseCommandBinding);