I wonder is there an API for creating custom notifications in Windows 10 style like the one showing when updates are available for example?
I know that Citrix can send messages showing like that and it seems they use the sessionmsg.exe. Unfortunately, I cannot find any help on the parameter the exe supports.
Also, an API would be preferred.
Another thing: How do you call this kind of message? Banner? Message? MessageBox? SystemMessage?
After reading the question, I found it interesting my selves to investigate possibilities and share my founds as the answer.
As you tagged your question with C#, my solution will be based on C#. By and btw I was not able to find the default api to do the job, if other finds a solution with an example I will be happy to vote for it.
I started with the UWP solution and created a very simple information dialog using ContentDialog. Create a UWP project and add the following code:
private async void ShowMessage()
{
ContentDialog dialog = new ContentDialog
{
Title = "Title",
Content = "Content text",
Width = 200,
Height = 600,
Background = new SolidColorBrush(Colors.CornflowerBlue),
Foreground = new SolidColorBrush(Colors.White),
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Colors.White),
CloseButtonText = "Close",
};
await dialog.ShowAsync();
}
public MainPage()
{
this.InitializeComponent();
ShowMessage();
}
This will create something like
But that content dialog appears as a part of the application and not the windows system as I tried to solve.
Adding the following lines before the ShowMessage method will maximize application to the whole background of the screen.
ApplicationView.GetForCurrentView().SuppressSystemOverlays = true;
ApplicationView.GetForCurrentView().FullScreenSystemOverlayMode = FullScreenSystemOverlayMode.Minimal;
ApplicationView.GetForCurrentView().TryEnterFullScreenMode();
But IMO it is not the best solution. I thought, there might be another way, I tried with WPF instead.
I created a WPF project, my strategy was to start MainWindow in minimized mode and the content dialog to appear. Hence there is no content dialog in WPF like UWP, I created something similar (look and feel).
Here is the code in my MainWindow
private readonly string _title;
private readonly string _message;
public MainWindow()
{
_title = "Updates are available";
_message = "Required updates need to be downloaded.";
InitializeComponent();
string[]? args = App.Args;
if (args != null && args.Length > 0)
{
_title = args[0];
_message = args[1];
}
MinimizedMainWindow();
ShowContentDialog();
}
protected void MinimizedMainWindow()
{
AllowsTransparency = true;
WindowStyle = WindowStyle.None;
WindowState = WindowState.Maximized;
Background = Brushes.Transparent;
Topmost = true;
}
public void ShowContentDialog()
{
ContentDialog dialog = new ContentDialog
{
Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF0066CC")),
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFFFFFFF")),
WindowStartupLocation = WindowStartupLocation.CenterScreen,
SizeToContent = SizeToContent.WidthAndHeight,
WindowStyle = WindowStyle.None,
Padding = new Thickness(20),
Margin = new Thickness(0),
ResizeMode = ResizeMode.NoResize,
Width = 600,
Height = 200,
Title = { Text = _title },
Message = { Text = _message }
};
dialog.Show();
}
And here is my ContentDialog.xaml
<Window x:Class="NotificationSol.ContentDialog"
xmlns:local="clr-namespace:NotificationSol"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:av="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="ContentDialog"
av:DesignWidth="600"
av:DesignHeight="200"
>
<Grid>
<TextBlock x:Name="Title" Margin="30,22,30,118" TextWrapping="Wrap" Text="Title" FontSize="28"/>
<TextBlock x:Name="Message" Margin="30,70,30,70" TextWrapping="Wrap" Text="Content" FontSize="16"/>
<Button x:Name="Button" Click="CloseButton_Click" Content="Close" HorizontalAlignment="Right" Margin="0,0,30,20" VerticalAlignment="Bottom" Width="75" Background="#FF0066CC" BorderBrush="White" Foreground="White" Padding="8,4"/>
</Grid>
</Window>
And ContentDialog.xaml.cs
public ContentDialog()
{
InitializeComponent();
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
base.OnClosed(e);
Application.Current.Shutdown();
}
To let my application take parameters in the command line, I made the following changes to App.xaml.cs:
public static string[]? Args;
void AppStartup(object sender, StartupEventArgs e)
{
if (e.Args.Length > 0)
{
Args = e.Args;
}
}
And to App.xaml adding as the startup
<Application x:Class="NotificationSol.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:NotificationSol"
StartupUri="MainWindow.xaml"
Startup="AppStartup">
<Application.Resources>
</Application.Resources>
</Application>
Running this code from the visual studio I get the:
If you run the software with 2 parameters from the command line, you can pass the title and the message. You have the code, where you can create other method actions or buttons with other features. Right now my button is just closing the dialog box and the application. Here I have put both examples on my repo:
https://github.com/maythamfahmi/BlogExamples/tree/master/Stackoverflow/ContentDialog
You need to create a function using Winforms and WPF assemblies to create the pop up.
Functions
Function New-WPFDialog() {
<#
.SYNOPSIS
This neat little function is based on the one from Brian Posey's Article on Powershell GUIs
.DESCRIPTION
I re-factored a bit to return the resulting XaML Reader and controls as a single, named collection.
.PARAMETER XamlData
XamlData - A string containing valid XaML data
.EXAMPLE
$MyForm = New-WPFDialog -XamlData $XaMLData
$MyForm.Exit.Add_Click({...})
$null = $MyForm.UI.Dispatcher.InvokeAsync{$MyForm.UI.ShowDialog()}.Wait()
.NOTES
Place additional notes here.
.LINK
http://www.windowsnetworking.com/articles-tutorials/netgeneral/building-powershell-gui-part2.html
.INPUTS
XamlData - A string containing valid XaML data
.OUTPUTS
a collection of WPF GUI objects.
#>
Param([Parameter(Mandatory = $True, HelpMessage = 'XaML Data defining a GUI', Position = 1)]
[string]$XamlData)
# Add WPF and Windows Forms assemblies
try {
Add-Type -AssemblyName PresentationCore, PresentationFramework, WindowsBase, system.windows.forms
}
catch {
Throw 'Failed to load Windows Presentation Framework assemblies.'
}
# Create an XML Object with the XaML data in it
[xml]$xmlWPF = $XamlData
# Create the XAML reader using a new XML node reader, UI is the only hard-coded object name here
Set-Variable -Name XaMLReader -Value #{ 'UI' = ([Windows.Markup.XamlReader]::Load((new-object -TypeName System.Xml.XmlNodeReader -ArgumentList $xmlWPF))) }
# Create hooks to each named object in the XAML reader
$Elements = $xmlWPF.SelectNodes('//*[#Name]')
ForEach ( $Element in $Elements ) {
$VarName = $Element.Name
$VarValue = $XaMLReader.UI.FindName($Element.Name)
$XaMLReader.Add($VarName, $VarValue)
}
return $XaMLReader
}
Function New-PopUpWindow () {
param(
[string]
$MessageText = "No Message Supplied")
# This is the XaML that defines the GUI.
$WPFXamL = #'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Popup" Background="#FF0066CC" Foreground="#FFFFFFFF" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" SizeToContent="WidthAndHeight" WindowStyle="None" Padding="20" Margin="0">
<Grid>
<Button Name="OKButton" Content="OK" HorizontalAlignment="Right" Margin="0,0,30,20" VerticalAlignment="Bottom" Width="75" Background="#FF0066CC" BorderBrush="White" Foreground="White" Padding="8,4"/>
<TextBlock Name="Message" Margin="100,60,100,80" TextWrapping="Wrap" Text="_CONTENT_" FontSize="36"/>
</Grid>
</Window>
'#
# Build Dialog
$WPFGui = New-WPFDialog -XamlData $WPFXaml
$WPFGui.Message.Text = $MessageText
$WPFGui.OKButton.Add_Click( { $WPFGui.UI.Close() })
$null = $WPFGUI.UI.Dispatcher.InvokeAsync{ $WPFGui.UI.ShowDialog() }.Wait()
}
Example call and result
New-PopUpWindow -MessageText "Hey there, I'm a pretty blue form"
Interested in that too.
Finished with simple WinForms C# app though I'm not in code. It covers every screen with 50% transparent black fullscreen form and then opens modal (OnShown TopMost) blue borderless 678x165 form from the primary screen form. Picked almost same colors and fonts (Segoe UI title + Calibri text and bold buttons) so it is pretty simple and similar phoney. And I've added second red button with some special functionality, what is of course impossible via standard API.
Another way is to know that such a message can be called with Send-RDUserMessage powershell cmdlet from RemoteDesktop module. I opened the module source code and understood that it uses wtsapi32.dll. I stopped pushing that direction because of:
RemoteDesktop module requires admin elevation, maybe wtsapi calls in general requires that too; but I want banner to launch in user context from task scheduler;
Cast Send-RDUserMessage on Windows 10 desktop OS results in small "msg.exe"-like (not sessionmsg.exe) messagebox but I want that banner on Windows 10 workstations too, not only on terminal servers. But workstations use the same banner to say about updates and activation so it is definitely not server unique function.
I am not into coding at all. Someone can try to get along with that dll, maybe WTSSendMessageA and WTSSendMessageW methods which are documented at learn.microsoft.com. Maybe it calls another API because such messages are not only for RD/TS purposes. But I got tired dealing with it.
Still interested in oneliner :)
Related
So I'm writing a WPF application with IronPython. Everything works great if I run the script outside of IronPython REPL via command "ipy.exe wpf.py". However, if the script were run inside IronPython REPL via command "execfile('wpf.py')", the first time it runs OK, the second time it errors out "SystemError: Cannot create more than one System.Windows.Application instance in the same AppDomain."
From my understanding, it's because it'll create a new AppDomain every time you run it outside REPL while it'll share the same domain when running inside REPL, and you can initialize Application twice. The problem is I have to run it inside the same AppDomain many times as it's not a standalone IronPython application. I've tried many things such as change shutdown mode by add app.ShutdownMode = ShutdownMode.OnExplicitShutdown after app=Application(), but that just hang the whole REPL.
Can someone please help shed some light? Thank you very much!
import clr
clr.AddReference("PresentationFramework")
clr.AddReference("PresentationCore")
clr.AddReference("System.Xml")
from System.Xml import XmlReader
from System.IO import StringReader
from System.Windows.Markup import XamlReader
from System.Windows import Application
s = XmlReader.Create(StringReader('''
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="IronPython MVVM Demo2"
Width="450"
SizeToContent="Height">
<Grid Margin="15" x:Name="grid1">
<StackPanel Margin="5">
<Button Margin="5">One</Button>
<Button Margin="5">Two</Button>
<Button Margin="5">Three</Button>
</StackPanel>
</Grid>
</Window>
'''))
win = XamlReader.Load(s)
app = Application()
app.Run(win)
print("The End.")
I believe you need to create a long-running STA thread to host the Applilcation, and communicate with it through the Applciations's Dispatcher. Here's an example in C#:
using System;
using System.IO;
using System.Threading;
using System.Windows;
using System.Xml;
namespace ConsoleApp1
{
class Program
{
static void ShowWindow(string Xaml)
{
var s = XmlReader.Create(new StringReader(Xaml));
var win = (Window)System.Windows.Markup.XamlReader.Load(s);
win.ShowDialog();
}
static void Main(string[] args)
{
Application app = null;
var UIThread = new Thread(() =>
{
app = new Application();
app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
app.Run();
});
UIThread.SetApartmentState(ApartmentState.STA);
UIThread.Start();
while (app == null )
Thread.Sleep(100);
app.Dispatcher.Invoke(() => Console.WriteLine("Started"));
var xaml = #"
<Window
xmlns = ""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
xmlns:x = ""http://schemas.microsoft.com/winfx/2006/xaml""
Title = ""IronPython MVVM Demo2""
Width = ""450""
SizeToContent = ""Height"">
<Grid Margin = ""15"" x:Name = ""grid1"">
<StackPanel Margin = ""5"">
<Button Margin = ""5""> One </Button>
<Button Margin = ""5""> Two </Button>
<Button Margin = ""5""> Three </Button>
</StackPanel>
</Grid>
</Window>";
for (int i = 0; i < 3; i++)
{
Application.Current.Dispatcher.Invoke(() =>
{
ShowWindow(xaml);
});
}
Application.Current.Dispatcher.Invoke(() =>
{
Application.Current.Shutdown();
});
}
}
}
I see some answer about it, suggest use Live Unit Testing feature.
So I don't have Microsoft Enterprise and therefore can't use its Live Unit Testing feature. Tried to create a simple application in order to compile & reload into the WPF container window.
skeleton code look like:
public void RecompileAndReloadPrj ()
{
Grid.Content = null;
ReleaseAsm()
RunMsBuild(targetProject);
Grid.Content = LoadComponetFromAsm(targetASM);
}
Unfortunately getting it to work has turned out to be a bit complicated... Does anyone have ready code that they could post, tips etc, or provide a link?
Yes, before couple years I wrote some code for WPF like you wish. But it's very basic and have a lot of issues around it.
But I suggest you, check the feature of Edit XAML while debugging. It's work well even without breakpoint. Just run project under debugging mode and edit the XAML file, even if your code load it by code behind. Even Save not required.
I list here the code and some comment about it:
MainWindow.xaml
<Window x:Class="RebuildAndReloadWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:RebuildAndReloadWPF"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800" Activated="Window_Activated" >
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition />
</Grid.RowDefinitions>
<Border CornerRadius="6" BorderBrush="Gray" Background="LightGray" BorderThickness="2" >
<StackPanel x:Name="___No_Name_" Background="LightYellow" Orientation="Horizontal">
<Button Click="Button_Click" >Reload</Button>
<CheckBox x:Name="chkReloadOnFocus" VerticalContentAlignment="Center" VerticalAlignment="Center" Content="Reload on focus" Margin="10,0,0,0"/>
<TextBlock x:Name="txtIndicator" VerticalAlignment="Center" Margin="10,0,0,0"/>
<Border x:Name="elmJustNowIndicator" BorderThickness="1" BorderBrush="Black" Background="Orange" CornerRadius="5" Height="21" Width="76" Visibility="Hidden" Margin="10,0,0,0">
<TextBlock Text="Just now" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</StackPanel>
</Border>
<ScrollViewer x:Name="PlaceHolder" Grid.Row="1"/>
</Grid>
</Window>
MainWindow.xaml.cs
using Microsoft.Build.BuildEngine;
using System;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
namespace RebuildAndReloadWPF
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
const string projectPath = #"C:\...\Some-Project.csproj";
const string libFileName = "Some-Lib.dll";
const string ClassName = "NameSpace.UserControlName";
private string projectFileName;
private string projectDirectoryPath;
private string projectBinPath;
private string logFilePath;
private string appDirectoryPath;
private DispatcherTimer indicatorTimer = new DispatcherTimer();
public MainWindow ()
{
projectFileName = Path.GetFileName(projectPath);
projectDirectoryPath = Path.GetDirectoryName(projectPath);
projectBinPath = projectDirectoryPath + #"\bin\Debug";
logFilePath = projectDirectoryPath + #"\bin\Debug\build.log";
appDirectoryPath = AppDomain.CurrentDomain.BaseDirectory;
indicatorTimer = new DispatcherTimer();
indicatorTimer.Interval = TimeSpan.FromMilliseconds(4000);
indicatorTimer.Tick += ( sender, e ) =>
{
elmJustNowIndicator.Visibility = Visibility.Hidden;
indicatorTimer.IsEnabled = false;
};
InitializeComponent();
}
public void ReloadContainer ()
{
PlaceHolder.Content = null;
bool result = RunMsBuild();
if (!result)
{
txtIndicator.Text = "Compile error, see in log file. Compile fail at: " + DateTime.Now.ToLongTimeString();
txtIndicator.Foreground = Brushes.Red;
return;
}
else
{
txtIndicator.Text = "Last build at: " + DateTime.Now.ToLongTimeString();
txtIndicator.Foreground = Brushes.Green;
}
try
{
File.Copy(projectBinPath + #"\" + libFileName, appDirectoryPath + libFileName, true);
}
catch (Exception ex)
{
MessageBox.Show("Can't copy compiled lib file: " + ex.Message);
throw;
}
elmJustNowIndicator.Visibility = Visibility.Visible;
indicatorTimer.IsEnabled = false;
indicatorTimer.IsEnabled = true;
try
{
PlaceHolder.Content = AsmLoad();
}
catch (Exception ex)
{
MessageBox.Show("Laod assembly error" + ex.Message);
}
GC.Collect();
}
public bool RunMsBuild ()
{
Engine eng = new Engine();
FileLogger logger = new FileLogger();
logger.Parameters = "logfile=" + logFilePath;
eng.RegisterLogger(logger);
bool bb = eng.BuildProjectFile(projectPath);
eng.UnregisterAllLoggers();
return bb;
}
public FrameworkElement AsmLoad ()
{
byte[] assemblyFileBUffer = File.ReadAllBytes(appDirectoryPath + #"\" + libFileName);
Assembly asm = AppDomain.CurrentDomain.Load(assemblyFileBUffer);
ContentControl container = (ContentControl)asm.CreateInstance(ClassName);
return (FrameworkElement)container.Content;
}
private void Window_Activated ( object sender, EventArgs e )
{
if (chkReloadOnFocus.IsChecked == true)
ReloadContainer();
}
private void Button_Click ( object sender, RoutedEventArgs e )
{
ReloadContainer();
}
}
}
Comments:
The above code is work only against UserControl class. You can
expand code to support also with Windows and Panel.
The above code make rebuild only when window of our app activated
(focus). You can expand it to respond file save. By use with
FileSystemWatcher. Notice, the watcher run event for every file. So
you need wait after all event burst end (brobebly by timer), and
also Suggest to configure Visual Studio to make always SaveALL for
Ctr+S key combination.
Microsoft replace Microsoft.Build.BuildEngine with newest assembly
and suggest use with new Microsoft.Build. I realize that newest have
problem find the newest Tools (MSBuild.exe) like version 15.0. You
probably will get error that need workaround:
Microsoft.Build.Exceptions.InvalidProjectFileException: 'The tools
version "15.0" is unrecognized. Available tools versions are "12.0",
"14.0", "2.0", "3.5", "4.0".'
Dotnet can't release assembly after loading it dynamically. Work
around not worth the effort. I check the above code and I run it
with loop, the consume RAM is better than I was imagined. And also,
I realize that Core 3.0 have solution for that. So if you like this
code, I suggest you try emigrate it to core 3.0
Performance. I not try it on real project. But if you have spare RAM
and strong CPU I believe that work well for small project, and not
have delay more than half of second. I compare it against start
debugging after code change. The Visual studio have a long delay to
enter debug mode and exit. So may you get significate improvement.
By the way, if you copy the debug info file (.pdb) as well as lib
file, the VS will open the source file when runtime error occur on
target projects. But this file get Lock. And next reload Fail (It's
weird, but according my check, it's not happened if target project
is VB).
If you really want develop with this approach. You need build your
target app as collection of small project. You can Embedded the
below code in your target project and open the container window when
you on develop mode. You can add Cache system for all data read from
files or outside resources like Databases. And Build system that
allow jump the reload directly to some point in the Interface.
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.
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);
I downloaded the Microsoft Visual Studio 2010 Express for Windows Phone and I wrote a simple application to make a first test of the emulator. In this application I have only a button with the property Content binded to a string called ButtonText and with the property Background binded to a SolidColorBrush named FillColor. I handled the Click event with this code:
void MyButton_Click(object sender, RoutedEventArgs e)
{
if (toggle == true)
{
ButtonText = "Blue";
FillColor = new SolidColorBrush(Colors.Blue);
}
else
{
ButtonText = "Red";
FillColor = new SolidColorBrush(Colors.Red);
}
toggle = !toggle;
}
Unfortunately this doesn't work. While the Content of the Button changes each time the button is pressed, I cannot say the same for the Background which remains at the same color.
Could you tell me what is wrong? Thank you.
I also post the XAML:
<Grid x:Name="ContentGrid" Grid.Row="1">
<Button Name="MyButton" Width="300" Height="300"
Content="{Binding Path=ButtonText}"
Background="{Binding Path=FillColor}" />
</Grid>
The issue is with the use of "new" in the line:
FillColor = new SolidColorBrush(Colors.Blue);
Using the "new" operation breaks the data binding that was previously set up. Try using the following instead:
FillColor.Color = Colors.Blue;
Replace both the changes to Blue and to Red and that should do the trick.
HTH!
Chris