Application Navigation through a single frame - c#

I am currently making an c# wpf application in which a task is need to do step-by-step through a single frame, like software installation. Has anyone got idea how to make it?

The way I did it on a recent application I made was to have the main window have 3 parts, a header, the main content, and a footer. Each one had a ContentControl that I bound so that in my ViewModel I could change the content being displayed dynamically. In the view model I loaded up the appropriate display based on the state of the program.
If I was at screen Welcome, I loaded up the appropriate header, footer, and content for the welcome screen. The XAML looked like this for each part:
<ContentControl x:Name="ContentHeader" Content="{Binding Path=ContentHeader}" Grid.Row="0"/>
<ContentControl x:Name="ContentMain" Content="{Binding Path=ContentMain}" Grid.Row="1"/>
<ContentControl x:Name="ContentFooter" Content="{Binding Path=ContentFooter}" Grid.Row="2"/>
In my code then I made new instances as necessary:
if (_screen == Constants.Screens.Welcome)
{
ContentHeader = new HeaderWelcome();
ContentMain = new MainWelcome();
ContentFooter = new FooterWelcome();
}
else if (_screen == Constants.Screens.Setup)
{
ContentHeader = new HeaderSetup();
ContentMain = new MainSetup();
ContentFooter = new FooterSetup();
}
Each part, MainWelcome, Main Setup, FooterWelcome, etc, are all just user controls I created.
This lets each of the parts have very little to do themselves. Buttons on those panels raised custom events to bubble up to the view model. Like so:
Define the custom event:
public static readonly RoutedEvent WelcomeEvent=
EventManager.RegisterRoutedEvent("WelcomeEvent", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(UserControl));
Call the custom event (from a button in this example):
private void ButtonWelcome_Click(object sender, RoutedEventArgs e)
{
RaiseEvent(new RoutedEventArgs(Events.TestTintEvent));
}
Finally, tie the event to a function in your view model. This can be done in the MainWindow's constructor:
AddHandler(Events.WelcomeEvent, new RoutedEventHandler(ViewModel.GetInstance().Welcome));
Then I just have the function Welcome defined in my view model and do the work there:
public void Welcome(object sender, RoutedEventArgs e)
{
//Do Work
}
This may not be the most optimal way to do it, but it was how I did it, and it worked well in my situation.

Related

Avalonia UserControl checking if it is visible before before acting on a timer

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

How to catch events from WindowsApiCodePack ExplorerBrowser control in viemodel?

I'm using CaliburnMicro in my WPF application, which uses an ExplorerBrowser control from WindowsAPICodePack (Microsoft-WindowsAPICodePack-Shell and Microsoft-WindowsAPICodePack-Core).
The events like SelectionChanged attached to this control are not firing in the viewmodel.
I've tried it multiple ways with Caliburn's [Event] = [Action()], or making a simpler "WinForms" style event to the backing class of the view - none of these worked.
Caliburn events are working fine for any other controls in the view. So if I place an event on the parent Grid - it works.
The only way I found it does work, is accessing the control itself by name from code-behind, which isn't how I want do things.
I'm also not entirely clear on the syntax for the Caliburn event in this case, because it's accessible through Selector - [Event Selector.SelectionChanged].
I also tried catching it with different arg types and other events with same result.
Here's the View:
<UserControl x:Class="App.WinExplorer.WinExplorerView"
xmlns:WindowsAPICodePackPresentation="clr-namespace:Microsoft.WindowsAPICodePack.Controls.WindowsPresentationFoundation;assembly=Microsoft.WindowsAPICodePack.Shell"
xmlns:cal="http://www.caliburnproject.org"
...
<Grid>
<WindowsAPICodePackPresentation:ExplorerBrowser
x:Name="ExplorerBrowser"
NavigationTarget="{Binding NavigationTarget}"
cal:Message.Attach="[Event Selector.SelectionChanged] = [Action SelectionChanged($this, $eventArgs)]"
/>
</Grid>
</UserControl>
Here's the handler in the viewmodel:
public void SelectionChanged(object s, SelectionChangedEventArgs e)
{
// never hits this method
}
Tried declaring the event the reular WPF way too:
Selector.SelectionChanged="ExplorerBrowser_SelectionChanged"
What actually works but is a backwards way of doing it - inside the code-behind:
public partial class WinExplorerView : UserControl
{
public WinExplorerView()
{
InitializeComponent();
ExplorerBrowser.SelectedItems.CollectionChanged += SelectedItems_CollectionChanged;
}
private void SelectedItems_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
throw new System.NotImplementedException();
}
}
Any insight would be appreciated.

Programmatically switching tabs in a Tab Control

I am wondering how to switch to a different tab within a tab control.
I have a main window that has a tab control associated with it and it directs to different pages. I want to switch to a tab from an event triggered within a different tab. When I try to use TabControl.SelectedIndex I get the error "An object reference is required to access non-static, method or property 'MainWindow.tabControl'
Here is my code declaring the TabControl from the MainWindow and trying to switch to it from a different tab.
<TabControl Name="tabControl" Margin="0,117,0,0" SelectionChanged="tabControl_SelectionChanged" Background="{x:Null}" BorderBrush="Black">
<TabItem x:Name="tabMO" Header="MO" IsTabStop="False">
<Viewbox x:Name="viewMO" Margin="0,0,0,0" Stretch="Fill" StretchDirection="Both">
<local:ManufacturingOrder x:Name="mo" Height="644" Width="1322"/>
</Viewbox>
</TabItem>
<TabItem x:Name="tabOptimize" Header="Optimize" IsTabStop="False">
<Viewbox x:Name="viewOptimize" Margin="0,0,0,0" Stretch="Fill" StretchDirection="Both">
<local:EngineeringOptimization x:Name="Optimize" Height="644" Width="1600"/>
</Viewbox>
</TabItem>
</TabControl>
private void dataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var cellInfo = dataGrid.SelectedCells[0];
var content = (cellInfo.Column.GetCellContent(cellInfo.Item) as TextBlock).Text;
var r = new Regex("[M][0-9]{6}");
if (r.IsMatch(content.ToString()))
{
MainWindow.tabControl.SelectedIndex = 4;
}
}
I have tried switching this to a private static void and received the same error.
I have also tried the following code, creating an instance of MainWindow, and there is no errors but when I run the code the selected tab doesn't change on the screen. But if I use a MessageBox to view the Selected Index, than I see my changed tab Index.
private void dataGrid_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var cellInfo = dataGrid.SelectedCells[0];
var content = (cellInfo.Column.GetCellContent(cellInfo.Item) as TextBlock).Text;
var r = new Regex("[M][0-9]{6}");
if (r.IsMatch(content.ToString()))
{
MainWindow frm = new MainWindow();
frm.tabControl.SelectedIndex = 4;
}
}
It looks like your main problem is that you do not have easy access to your MainWindow and all of its children from within your ManufacturingOrder or EngineeringOptimization UserControls. Which is normal. There are a few ways around this. A simple one, which violates some MVVM principles, (but you're doing that anyway, so I don't think you'll mind) is to retrieve the instance of your MainWindow object:
//Loop through each open window in your current application.
foreach (var Window in App.Current.Windows)
{
//Check if it is the same type as your MainWindow
if (Window.GetType() == typeof(MainWindow))
{
MainWindow mWnd = (MainWindow)Window;
mWnd.tabControl.SelectedIndex = 4;
}
}
Once you retrieve the running instance of your MainWindow, then you have access to all its members. This has been tested as well as possible without access to your specific custom UserControls and instances. But it's a pretty standard problem and solution.
You were on the right track with your last bit of code in your question, but you were creating a 'new' instance of your MainWindow. You have to retrieve the current running instance, not a new instance.

XAML DependencyProperty object notify parent on new instance

I have DependecyProperty (named Block) on a XAML UserControl (named BlockPresenter) which expects a data type of IBlock. This IBlock intefae has a Visual GetControl() method - amongst other things - which returns an element of some kind to allow the user to edit the values for that specific block. The BlockPresenter provides a way of choosing which block to use (e.g. BlockA, BlockB, etc.) and also shows the GetControl return valued for the block itself.
There are models which contain one or more IBlock properties under varying different names. Each of these models has their own user control which has the model as it's DataContext. In the XAML for the model's control, I have lines such as <controls:BlockPresenter Block="{Binding Foo}" /> to show the control and allow the user to change the block type or the properties for their chosen block.
When the user changes block type from the dropdown in BlockPresenter, a new instance of the selected block is created and set as the Block DependencyProperty and the presenter calls GetControl for the new Block. This is fine, however the problem is that this does not send the new value up to the model.
BlockPresenter.xaml
<Border BorderThickness="2" BorderBrush="#F00">
<StackPanel>
<ComboBox x:Name="BlockSelection" SelectedValuePath="BlockType" DisplayMemberPath="Name" SelectionChanged="BlockSelection_SelectionChanged" />
<ContentControl x:Name="ContentContainer" Margin="0" />
</StackPanel>
</Border>
BlockPresenter.xaml.cs
public BlockPresenter() {
InitializeComponent();
BlockSelection.ItemsSource = new List<BlockListItem> {
new BlockListItem("BlockA", typeof(BlockA)),
new BlockListItem("BlockB", typeof(BlockB)),
// BlockListItem is a simple class containing just "Name" and "BlockType" as read-only auto properties.
};
}
private static void OnBlockChange(DependencyObject conditionPresenter, DependencyPropertyChangedEventArgs eventArgs) {
var control = (BlockPresenter)conditionPresenter;
var newBlock = (IBlock)eventArgs.NewValue;
control.ContentContainer.Content = newBlock?.GetControl();
control.BlockSelection.SelectedValue = eventArgs.NewValue?.GetType();
}
public static readonly DependencyProperty BlockProperty = DependencyProperty.Register("Block", typeof(IBlock), typeof(BlockPresenter), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnBlockChange));
public IBlock Block {
get => (IBlock)GetValue(BlockProperty);
set => SetValue(BlockProperty, value);
}
private void BlockSelection_SelectionChanged(object sender, SelectionChangedEventArgs e) {
Block = (IBlock)Activator.CreateInstance(BlockSelection.SelectedValue);
}
DefaultModelControl.xaml
<Grid>
<Grid.ColumnDefinitions>
...
</Grid.ColumnDefitions>
<Label Content="Main Block:" Grid.Row="0" />
<logic:BlockPresenter Block="{Binding MainBlock}" Grid.Row="0" Grid.Column="1" />
<Label Content="Subblock:" Grid.Row="1" />
<logic:BlockPresenter Block="{Binding SubBlock}" Grid.Row="1" Grid.Column="1" />
</Grid>
When the main or sub block types are changed for the default model, the value inside the model (MainBlock or SubBlock properties) are not updated to reflect the newly selected block.
I know one solution could be to raise a custom event (e.g. BlockChanged) from the BlockPresenter when a new value is set, passing this value as event args and then capturing this event in the DefaultModelControl.xaml.cs file and setting the property to the new reference.
I was wondering if there is another way of doing this without the listeners though, since it's annoying to have to create them for each block presenter I want.
Am I going about this wrong? Would love to hear peoples constructive thoughts on this. Thanks :)

WPF - How to persist current content when setting the content property

I'm facing a problem where by setting the content property of my window I obviously remove pre-existing content. On all windows I have a dockpanel that I use to pop up help contextual help to the user but this is lost when I set the content property of the window. Therefore I will only see the content for the control I've added and pressing F1 does nothing as the dockpanel does not exist. I don't want to add this dockpanel to every control as it's poor code-reuse so what can I do to keep the dockpanel on the window and add content without overwriting original content of the window?
This is the code where I set the content of the window.
private void btnHelp_Click(object sender, RibbonControlEventArgs e)
{
System.Windows.Window window = new ResizeableWindow()
{
Title = "Help",
Content = new Controls.Help(),
ResizeMode = ResizeMode.NoResize
};
window.ShowDialog();
}
This is code for my Help control it's just a document viewer to read an xps document, this is used by the dockpanel.
public partial class Help : UserControl
{
public Help()
{
InitializeComponent();
string appPath = "path";
XpsDocument doc = new XpsDocument(appPath, FileAccess.Read);
var docx = doc.GetFixedDocumentSequence();
HelpDocViewer.Document = docx;
}
}
This is the xaml of my ResizableWindow containing the Dockpanel
<Window x:Class="Controls.ResizeableWindow"
KeyDown="HelpKeyListen">
<Grid>
<DockPanel x:Name="HelpPanel">
</DockPanel>
</Grid>
</Window>
Here is the code for the resizeable window
public ResizeableWindow()
{
InitializeComponent();
}
private void HelpKeyListen(object sender, KeyEventArgs e)
{
if (e.Key == Key.F1)
{
var HelpControl = new Help();
DockPanel.SetDock(HelpControl, Dock.Right);
HelpPanel.Children.Insert(0, HelpControl);
}
}
Use Placeholders inside the DockPanel instead of replacing the window content:
<DockPanel x:Name="HelpPanel">
<ContentControl x:Name="HelpContent" DockPanel.Dock="Right"/>
<ContentControl x:Name="MainContent"/>
</DockPanel>
Then assign the contents of the contentcontrols as needed
private void HelpKeyListen(object sender, KeyEventArgs e)
{
if (e.Key == Key.F1)
{
HelpContent.Content = new Help();
}
}
Possibly create a new dependency property in ResizeableWindow if you want to provide main content from the outside. Lets say you add a dependency property (visual studio code snipped propdp) named MainContent, then you can bind it as follows:
<DockPanel x:Name="HelpPanel">
<ContentControl x:Name="HelpContent" DockPanel.Dock="Right"/>
<ContentControl x:Name="MainContentPlaceholder" Content="{Binding MainContent,RelativeSource={RelativeSource AnchestorType=Window}}"/>
</DockPanel>
The more appropriate option would be to replace the MainContentPlaceholder by some more WPF/MVVM friendly way to display your contents, but thats out of scope for the question.

Categories