My application has a menu option to allow the creation of a new account. The menu option's command is bound to a command (NewAccountCommand) in my ViewModel. When the user clicks the option to create a new account, the app displays a "New Account" dialog where the user can enter such data as Name, Address, etc... and then clicks "Ok" to close the dialog and create the new account.
I know my code in the ViewModel is not correct because it creates the "New Account" dialog and calls ShowDialog(). Here is a snippet from the VM:
var modelResult = newAccountDialog.ShowDialog();
if (modelResult == true)
{
//Create the new account
}
how do i avoid creating and showing the dialog from within my VM so I can unit test the VM?
I like the approach explained in this codeproject article:
http://www.codeproject.com/KB/WPF/XAMLDialog.aspx
It basically creates a WPF Dialog control that can be embedded in the visual tree of another window or usercontrol.
It then uses a style trigger that causes the dialog to open up whenever there is content in the dialog.
so in you xaml all you have to do is this(where DialogViewModel is a property in you ViewModel):
<MyControls:Dialog Content = {Binding DialogViewModel}/>
and in you ViewModel you just have to do the following:
DialogViewModel = new MyDialogViewModel();
so in unit testing all you have to do is:
MyViewModel model = new MyViewModel();
model.DialogViewModel = new MyDialogViewModel();
model.DialogViewModel.InputProperty = "Here's my input";
//Assert whatever you want...
I personally create a ICommand property in my ViewModel that sets the DialogViewModel property, so that the user can push a button to get the dialog to open up.
So my ViewModel never calls a dialog it just instantiates a property. The view interprets that and display a dialog box. The beauty behind this is that if you decide to change your view at all and maybe not display a dialog, your ViewModel does not have to change one bit. It pushes all the User interaction code where it should be...in the view. And creating a wpf control allows me to re-use it whenever I need to...
There are many ways to do this, this is one I found to be good for me. :)
In scenarios like this, I typically use events. The model can raise an event to ask for information and anybody can respond to it. The view would listen for the event and display the dialog.
public class MyModel
{
public void DoSomething()
{
var e = new SomeQuestionEventArgs();
OnSomeQuestion(e);
if (e.Handled)
mTheAnswer = e.TheAnswer;
}
private string mTheAnswer;
public string TheAnswer
{
get { return mTheAnswer; }
}
public delegate void SomeQuestionHandler(object sender, SomeQuestionEventArgs e);
public event SomeQuestionHandler SomeQuestion;
protected virtual void OnSomeQuestion(SomeQuestionEventArgs e)
{
if (SomeQuestion == null) return;
SomeQuestion(this, e);
}
}
public class SomeQuestionEventArgs
: EventArgs
{
private bool mHandled = false;
public bool Handled
{
get { return mHandled; }
set { mHandled = value; }
}
private string mTheAnswer;
public string TheAnswer
{
get { return mTheAnswer; }
set { mTheAnswer = value; }
}
}
public class MyView
{
private MyModel mModel;
public MyModel Model
{
get { return mModel; }
set
{
if (mModel != null)
mModel.SomeQuestion -= new MyModel.SomeQuestionHandler(mModel_SomeQuestion);
mModel = value;
if (mModel != null)
mModel.SomeQuestion += new MyModel.SomeQuestionHandler(mModel_SomeQuestion);
}
}
void mModel_SomeQuestion(object sender, SomeQuestionEventArgs e)
{
var dlg = new MyDlg();
if (dlg.ShowDialog() != DialogResult.OK) return;
e.Handled = true;
e.TheAnswer = dlg.TheAnswer;
}
}
The WPF Application Framework (WAF) shows a concrete example how to accomplish this.
The ViewModel sample application shows an Email Client in which you can open the “Email Account Settings” dialog. It uses dependency injection (MEF) and so you are still able to unit test the ViewModel.
Hope this helps.
jbe
There are different approaches to this. One common approach is to use some form of dependency injection to inject a dialog service, and use the service.
This allows any implementation of that service (ie: a different view) to be plugged in at runtime, and does give you some decoupling from the ViewModel to View.
Related
I'm getting started with WinUI3. Having no previous experience with UWP or WPF or even much C#, it's tough going.
Today's question is how to display a simple dialog at startup. Consider that we start with a simple app, as generated by Visual Studio. We have a MainWindow class defined in MainWindow.xaml.cs and its associated XAML (MainWindow.xaml). I believe the class is called a code-behind class.
So I want to do something as simple as display a dialog when the (desktop) app runs. It looks as though a ContentDialog is the way to go. But how to display it? As I understand it, I'm going to need to set the XamlRoot, so naively I tried this:
public MainWindow()
{
this.InitializeComponent();
DisplayDialog();
}
private async void DisplayDialog()
{
var dlg = new ContentDialog();
dlg.XamlRoot = this.Content.XamlRoot; // <-- set the XAML root here, but it's null
dlg.Content = "Hello World";
await dlg.ShowAsync();
}
This doesn't work. When called, the main window's XAML root is null and trying to show the dialog throws an exception:
How do I detect when it's ok to use the main window's XAML root? This issue seems to hint at an OnLoaded event, but I can't find anything about OnLoaded events in WinUI. Did I miss something?
This only way I can get this to work is to hook into the window's button and respond to its Loaded event, i.e.
public MainWindow()
{
this.InitializeComponent();
myButton.Loaded += MyButton_Loaded; // <-- hack
}
private void MyButton_Loaded(object sender, RoutedEventArgs e)
{
DisplayDialog();
}
private async void DisplayDialog()
{
var dlg = new ContentDialog();
dlg.XamlRoot = this.Content.XamlRoot; // <-- this is non-null now!
dlg.Content = "Hello World";
await dlg.ShowAsync();
}
But this feels really dirty. I don't even know if it's guaranteed that the XamlRoot will be non-null just because a button has loaded. And anyway, latching onto the button load seems very much like a hack. It relies on there being a button for one thing!
So how should I achieve the simple task of putting a dialog on the screen when all I have is the main window?
All help very gratefully received. Please try to make any answers as newbie-friendly as possible.
Unfortunately, the MainWindow has no Loaded events. What you can do is to work with a Page instead. So basically use the MainWindow as a "Window" and use Pages for your contents.
MainPage.xaml.cs
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
this.Loaded += MainPage_Loaded;
}
private async void MainPage_Loaded(object sender, RoutedEventArgs e)
{
var dlg = new ContentDialog();
dlg.XamlRoot = this.XamlRoot;
dlg.Content = "Hello World";
await dlg.ShowAsync();
}
}
MainWindow.xaml
<Window
x:Class="ContentDialogs.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:local="using:ContentDialogs"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<local:MainPage />
</Window>
A WinUI Window is just an abstraction of each of the low-level window implementations used by supported (UWP and desktop) app models.
The "trick" is to handle the Loaded event of the window's root element. The default template includes a Grid root element. A "generic" solution could be implemented something like this:
public MainWindow()
{
this.InitializeComponent();
var root = this.Content as FrameworkElement;
if (root != null)
root.Loaded += async (s, e) => await DisplayDialog();
}
private async Task DisplayDialog()
{
var dlg = new ContentDialog();
dlg.XamlRoot = this.Content.XamlRoot;
dlg.Content = "Hello World";
await dlg.ShowAsync();
}
Also tote that an async method, with the exception of event handlers, should return a Task or a Task<T> and be awaited by the caller.
I am coding a Xamarin app and doing my best to adhere to MVVM, which I actually really like
I commonly have ContentPages containing references to Views.
I set the binding context to a VM in the Page, and then make use of OnBindingContextChanged in the view
This allows me to use PropertyChanged method to then respond to display logic conditions for my View
I've used it several times successfully but I am baffled why an additional implementation isn't working
Page looks like this
public partial class BindingTextPage : ContentPage
{
public BindingTextPage()
{
InitializeComponent();
this.BindingContext = new ViewModels.LocationsViewModel();
}
}
View looks like this
private LocationsViewModel_vm;
public BindingTestView()
{
InitializeComponent();
System.Diagnostics.Debug.WriteLine("Debug: Initialised BindingTesView view");
}
protected override void OnBindingContextChanged()
{
System.Diagnostics.Debug.WriteLine("Debug: BindingTest: OnBindingContextChanged: Context " + this.BindingContext.GetType());
_vm = BindingContext as LocationsViewModel;
_vm.PropertyChanged += _vm_PropertyChanged;
}
private void _vm_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
try
{
System.Diagnostics.Debug.WriteLine("Debug: BindingTest: Method called");
System.Diagnostics.Debug.WriteLine("Debug: BindingTest: Property " + e.PropertyName);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Debug: BindingTestView: Error changing context " + ex.Message);
}
}
Extract of view model, very simply in this case setting a string and hence changing a property, which I would have expected would then cause PropertyChange to fire?
public LocationsViewModel()
{
tempMessage = "this is from the view model";
}
public string tempMessage
{
get
{
return _tempMessage;
}
set
{
_tempMessage = value;
OnPropertyChanged(nameof(tempMessage));
}
}
My debug statements when it boots up shows that OnBindingContextChange is being called, but in this one instance _vm_PropertyChanged never fires? I'd expect tempMessage being set to do so?
The order of events in your code is the following
Constructor of LocationsViewModel is called
From your constructor, you are setting tempMessage
The setter of tempMessage calls OnPropertyChanged, since the event is null at the time being, it's not fired
Constructor of LocationsViewModel is left
Page.BindingContext is set
OnBindingContextChanged is called
LocationsViewModel.PropertyChanged is subscribed by your page
Since the event is raised (or it's tried to) before your page subscribes to, your page simply does not get informed about the event being raised. If you set the value after the event has been subscribed to, the handler will be called as expected.
e.g.
protected override void OnBindingContextChanged()
{
_vm = BindingContext as LocationsViewModel;
_vm.PropertyChanged += _vm_PropertyChanged;
_vm.tempMessage = "Hello, world!"; // clichée , I know
}
I'm trying to add a customized UI page to Sparx EA. It provides adding ActiveX controls via scripting. Using JScript, I've done this, but since ActiveX has to be registered on each client, I'd rather use Microsoft Forms, already installed on all clients.
I've successfully built the UI, appearance wise, by adding a "Forms.Form.1" ActiveX object, and adding text boxes, labels & buttons to the controls property of the created form.
These objects support events, but I can't figure out how to assign an event handler.
Here is the JScript code I used to get the screen layout:
function _addControl(parentControl, controlProgId, controlName, left, top, width, height){
var newControl = parentControl.controls.add(controlProgId, controlName,1);
newControl.Name=controlName;
newControl._SetLeft(left);
newControl._SetTop(top);
newControl._SetWidth(width);
newControl._SetHeight(height);
return newControl;
}
function main(){
//Create main form
var form = Repository.AddTab("ScriptedForm", "Forms.Form.1");
if (null != form){
//Add control
var textBox1 = _addControl(form, "Forms.TextBox.1","TextBox1", 18,21,94,93);
var textBox2 = _addControl(form, "Forms.TextBox.1","TextBox2", 120, 21, 91, 93);
var btnTest = _addControl(form, "Forms.CommandButton.1", "btnTest", 60, 140, 90, 30);
btnTest.Caption = "Test";
//Here's where I assign the click event, but it's unhappy.
btnTest.add_Click(this.TextBox1_Click);
}
}
function TextBox1_Click(Object){
Session.Prompt("Click", promptOK);
}
The add_Click event expects a parameter of type CommandButtonEvents_ClickEventHandler.
There's nothing I can create that could be submitted as the parameter. I tried creating a JScript class duplicating the interface, but no joy.
I think you are running into several problems here at once.
(1) Process Lifetime
As far as I understand your question and its context, you are somehow manually executing a JScript script. Doing this EA will internally start SScripter.exe. You see this in the Debug window:
The process is effectively terminated after the script finishes (and thus also terminating any event handlers you might have registered in your UserControl or Form object).
(2) Passing a JScript object instance as a .NET delegate
If you could somehow extend the lifetime of the script environment, and if you could pass something to your event you will realise that any object in your JScript code will be passed as a System.__ComObject to the .NET runtime inside EA. Therefore you cannot just register an event handler.
However when you evaluate the object from .NET you will find out it is not an IDispatch interface:
MemberNames:
ToString,
GetLifetimeService,
InitializeLifetimeService,
CreateObjRef,
Equals,
GetHashCode,
GetType
TargetInvocationException#mscorlib: 'COM target does not implement IDispatch.'
I did a small test with the code below:
function MyClass(name)
{
this.name = name;
}
MyClass.prototype.Invoke = function(value)
{
Session.Output("name " + value);
return true;
}
function main()
{
var myClass = new MyClass("Hotzenplotz");
myClass.Invoke("some Value");
var ctrl = new ActiveXObject("IMASE.TestUserControl2");
ctrl.Repository = Repository;
ctrl.JavaScriptObject = myClass;
}
[ProgId(Global.ADDIN_NAME + Global.DOT + "TestUserControl2")]
[Guid("87156dd9-e947-44bf-92a9-e9554a5b1844")]
[ComVisible(true)]
public partial class TestUserControl2 : ActivexControl
{
public static string TabName { get; } = Global.ADDIN_NAME;
private static readonly Lazy<string> _controlId = new Lazy<string>(() =>
{
var attribute = typeof(TestUserControl).GetCustomAttribute<ProgIdAttribute>();
return attribute.Value;
});
private Timer timer;
public static string ControlId = _controlId.Value;
public Repository Repository { get; set; }
public object JavaScriptObject { get; set; }
public TestUserControl2()
{
timer = new Timer();
timer.Elapsed += TimerEvent;
timer.Interval = 5000;
timer.Enabled = true;
timer.Start();
}
~TestUserControl2()
{
Logger.Default.TraceInformation("I'm gonna die ... " + this.GetHashCode());
}
private void OnDispose(object sender, EventArgs e)
{
timer.Dispose();
}
private void TimerEvent(object source, ElapsedEventArgs e)
{
Logger.Default.TraceInformation("I'm still alive ... " + this.GetHashCode());
if(null == JavaScriptObject) return;
try
{
var memberNames = JavaScriptObject.GetType().GetMembers(BindingFlags.Instance|BindingFlags.FlattenHierarchy|BindingFlags.Public).Select(p => p.Name);
Logger.Default.TraceInformation("memberNames: " + string.Join(", ", memberNames));
var result = JavaScriptObject.GetType().InvokeMember("Invoke", BindingFlags.InvokeMethod, null, JavaScriptObject, new object[] {"arbitraryString"});
Logger.Default.TraceInformation("result: " + result);
}
catch (Exception ex)
{
Logger.Default.TraceException(ex);
}
}
}
(3) Another approach
Create a UserControl inside your addin (using WinForm or Forms) and use ClearScript as a ScriptEngine.
Pass Session and Repository from a EA script to your control (or do it otherwise such as a menu to wprk around the lifetime problem) and have either your forms code load a script from the repository (or any other source). Then react on your event handlers to execute your JScript code as needed. I create a simple example that shows how to call a control from an EA JScript and call another JScript from inside your forms code that in turn will log to the Debug session or regular scripting output window:
function main()
{
var ctrl = new ActiveXObject("IMASE.TestUserControl2");
ctrl.Repository = Repository;
ctrl.Session = Session;
Session.Prompt("wait", promptOK);
}
main();
Inside your form code you invoke your JScript with Repository and other objects like this:
public Repository Repository { get; set; }
public object Session { get; set; }
using (var engine = new JScriptEngine())
{
engine.AddHostObject("Repository", this.Repository);
engine.AddHostObject("Session", this.Session);
engine.Execute("Session.Output('Repository.ConnectionString: ' + Repository.ConnectionString);");
}
Here is an output of the above scripting interactions:
Side note: I personally do not see the need for using forms as we can dynamically register ActiveX controls at AddIn startup. For code on doing this you can have a look at the following gist:
https://gist.github.com/dfch/6a27bb1b9320c93456cee6d5b2b9d551
In addition, if you are using ClearScript as the script host, you can directly connect to your (UI) events from your script code as described in question #16 of the ClearScript FAQtorial.
I am currently programming using the MVVM pattern.
My view Model Looks like so
class DoorsViewModel
{
ObservableCollection<Door> doorCollection;
};
the door class looks like the following
class Door
{
string name;
bool isOpen;
};
my view is linked to the viewmodel, and simply contains a longlistselector with a picture and the name of the door. I want the picture to be dynamic and change depending on the state of the door( whether it's open or closed ). How would i implement it so that the picture updates dynamically depending on the state of the door? should this be done in the viewmodel? or should it be done within the view?
This logic should be in the ViewModel. All logic that is related to the view or how things are displayed should be in the ViewModel. No logic should be in the view (.xaml.cs).
You typically use the INotifyPropertyChanged interface to notify the View that something has changed. In this case you want a door image to be changed when the state of the door changes. In this case I would try something like this.
class Door: INotifyPropertyChanged
{
private string _name;
private bool _isOpen;
public Uri DoorImage
{
get
{
if (_isOpen) return new Uri("uri_to_open.png");
return new Uri("uri_to_closed.png");
}
}
public bool IsOpen
{
get { return _isOpen; }
set
{
_isOpen = value;
RaisePropertyChanged("IsOpen");
// important, notifies the UI to update the door image
RaisePropertyChanged("DoorImage");
}
}
private void RaisePropertyChanged(string propertyName)
{
var tmp = PropertyChanged;
if (tmp != null) tmp(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
};
Note: I have encapsulated the fields into properties.
If your image is embedded in your assebmly, please check out this link to learn how to write an uri for your image.
The Navigation framework in Windows Phone 7 is a cut down version of what is in Silverlight. You can only navigate to a Uri and not pass in a view. Since the NavigationService is tied to the View, how do people get this to fit into MVVM. For example:
public class ViewModel : IViewModel
{
private IUnityContainer container;
private IView view;
public ViewModel(IUnityContainer container, IView view)
{
this.container = container;
this.view = view;
}
public ICommand GoToNextPageCommand { get { ... } }
public IView { get { return this.view; } }
public void GoToNextPage()
{
// What do I put here.
}
}
public class View : PhoneApplicationPage, IView
{
...
public void SetModel(IViewModel model) { ... }
}
I am using the Unity IOC container. I have to resolve my view model first and then use the View property to get hold of the view and then show it. However using the NavigationService, I have to pass in a view Uri. There is no way for me to create the view model first. Is there a way to get around this.
Instead of passing the view through the constructor. You could construct the view first via the NavigationService and pass it into the view-model. Like so:
public class ViewModel : IViewModel
{
private IUnityContainer container;
private IView view;
public ViewModel(IUnityContainer container)
{
this.container = container;
}
public ICommand GoToNextPageCommand { get { ... } }
public IView
{
get { return this.view; }
set { this.view = value; this.view.SetModel(this); }
}
public void GoToNextPage()
{
// What do I put here.
}
}
PhoneApplicationFrame frame = Application.Current.RootVisual;
bool success = frame.Navigate(new Uri("View Uri"));
if (success)
{
// I'm not sure if the frame's Content property will give you the current view.
IView view = (IView)frame.Content;
IViewModel viewModel = this.unityContainer.Resolve<IViewModel>();
viewModel.View = view;
}
If you are using Mvvm Light you could try:
Windows Phone 7 — Navigation between pages using MVVM Light Messaging
(See similar post: Silverlight Navigation using Mvvm-light(oobe)+MEF?)
My opinion is that the view-model should be created and registered at application startup. By placing it inside the root DataContext all pages will automatically get a reference to it without any code-behind or IoC tricks.
// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
m_ViewModel = new PrimaryViewModel(RootFrame) ;
RootFrame.DataContext = m_ViewModel;
}
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
m_ViewModel = new PrimaryViewModel(RootFrame) ;
m_ViewModel.Activated(PhoneApplicationService.Current.State);
RootFrame.DataContext = m_ViewModel;
}
If you are using MVVM architecture,then you can pass navigationPage after registering using Messenger. Create a model class (say NavigateToPageMessage) with a string(say PageName) variable. You want to pass string from homepage.xaml to newpage.xaml,then in Homepage viewmodel just send the message like this under the command you binded (say HomeNavigationCommand)
private void HomeNavigationCommandHandler()
{
Messenger.Default.Send(new NavigateToPageMessage {PageName = "newpage"});
}
In the newpage Viewmodel,you should register the messenger like this,
Messenger.Default.Register<NavigateToPageMessage>(this, (action) => ReceiveMessage(action));
private object ReceiveMessage(NavigateToPageMessage action)
{
var page = string.Format("/Views/{0}.xaml", action.PageName);
NavigationService.Navigate(new System.Uri(page,System.UriKind.Relative));
return null;
}
//Assuming your views are in View Folder