I'm trying to use the mvvm-light messenger capability to open a custom confirm password dialog in my view, triggered by a command in my viewmodel.
I think I understand the usage of Messenger.Default.Register and Messenger.Default.Send.
But how do I get the dialog results back in my viewmodel?
To me the sending seems to be a one way street...
Could someone help a beginner with a small C#/WPF code sample?
Thanks for any help
IMHO it is better to use the NotificationMessageAction<T> as it is cut out for this task.
On the sender side:
var msg = new NotificationMessageAction<MessageBoxResult>(this, "GetPassword", (r) =>
{
if (r == MessageBoxResult.OK)
{
// do stuff
}
});
Messenger.Default.Send(msg);
And on the receiver side:
Messenger.Default.Register<NotificationMessageAction<MessageBoxResult>>(this, (m) =>
{
if (m.Notification == "GetPassword") {
var dlg = new PasswordDialog();
var result = dlg.ShowDialog();
m.Execute(result);
}
});
I believe that this approach is cleaner as it does not create an unnecessary dependency from the View to the ViewModel (although this way round is not so bad). For better readability consider sub-classing the NodificationMessageAction<MessageResult>. I.e.
public class ShowPasswordMessage : NotificationMessageAction<MessageBoxResult>
{
public ShowPasswordMessage(object Sender, Action<MessageBoxResult> callback)
: base(sender, "GetPassword", callback)
{
}
}
Then the sender
var msg = new ShowPasswordMessage(this, (r) =>
{
if (r == MessageBoxResult.OK)
{
// do stuff
}
});
Messenger.Default.Send(msg);
and receiver side
Messenger.Default.Register<ShowPasswordMessage>(this, (m) =>
{
var dlg = new PasswordDialog();
var result = dlg.ShowDialog();
m.Execute(result);
});
becomes a lot clearer.
And verry important unregister the recipient as else you might create a memory leak.
In Register method you can show a dialog and pass the YourViewModel reference.
Messenger.Default.Register<YourViewModel>(this, "showDialog", viewModel=>
{
var dlg = new Dialog();
viewModel.Result = dlg.ShowDialog();
});
somewhere in your code you can throw Send() message with a reference to YourViewModel like this:
Messenger.Default.Send(viewModel, "showDialog");
In order to achieve the above using DialogMessage as the title suggests,
one may use the following:
sender side:
void SendMessage(String msgText)
{
DialogMessage messege = new DialogMessage(msgText, res =>
{
callback(res);
})
//set more dialog properties using the Initializer
{ Button = MessageBoxButton.OKCancel, Caption = "" };
Messenger.Default.Send(messege, "mb1");
}
public void callback(MessageBoxResult res)
{
if (res == MessageBoxResult.OK)
{ /*do something*/ }
}
receiver side:
void Rec()
{
Messenger.Default.Register<DialogMessage>(
this, "mb1", msg => ShowDialog(msg));
Unloaded += Unreg;
}
void ShowDialog(DialogMessage msg)
{
var result = MessageBox.Show(
msg.Content,
msg.Caption,
msg.Button);
msg.Callback(result);
}
note the explicit call to the Callback method in the last line of the receiver.
msg.Callback(result);
Related
I have a program that searches the given directory and adds all the files to a list view. My problem is that the ui thread gets stuck while the search is busy. I have tried using tasks but can’t get it to work in async. The list view must be updated after each file has been found.
I have done a lot of reading about the TPL and how to use it but can’t get it to work in this case. I got it to work where the processing of data is in one method that create a task to process it. Can any one tel me what is wrong in the code below and how to fix it?
Here is my code:
private void button1_Click(object sender, EventArgs e)
{
Task.Run(() =>
{
WalkDirectory(new DirectoryInfo(drive));
});
}
public void testTaskUpdateLabel(string labelTeks)
{
Task taskUpdateLabel = new Task(() =>
{
label4.Text = labelTeks;
});
taskUpdateLabel.Start(uiScheduler);
}
public void testTaskUpdateLabel(string labelTeks)
{
Task taskUpdateLabel = new Task(() =>
{
label4.Text = labelTeks;
});
taskUpdateLabel.Start(uiScheduler);
}
public bool WalkDirectory(DirectoryInfo directory)
{
if (directory == null)
{
throw new ArgumentNullException("directory");
}
return this.WalkDirectories(directory);
}
private bool WalkDirectories(DirectoryInfo directory)
{
bool continueScan = true;
continueScan = WalkFilesInDirectory(directory);
if (continueScan)
{
DirectoryInfo[] subDirectories = directory.GetDirectories();
foreach (DirectoryInfo subDirectory in subDirectories)
{
try
{
if ((subDirectory.Attributes & FileAttributes.ReparsePoint) != 0)
{
continue;
}
if (!(continueScan = WalkDirectory(subDirectory)))
{
break;
}
}
catch (UnauthorizedAccessException)
{
continue;
}
}
}
if (continueScan)
{
testTaskUpdateLabel(directory.FullName);
}
return continueScan;
}
private bool WalkFilesInDirectory(DirectoryInfo directory)
{
bool continueScan = true;
// Break up the search pattern in separate patterns
string[] searchPatterns = _searchPattern.Split(';');
// Try to find files for each search pattern
foreach (string searchPattern in searchPatterns)
{
if (!continueScan)
{
break;
}
// Scan all files in the current path
foreach (FileInfo file in directory.GetFiles(searchPattern))
{
try
{
testTaskUpdate(file.FullName);
}
catch (UnauthorizedAccessException)
{
continue;
}
}
}
return continueScan;
}
If you use a BackgroundWorker class, the UI will work and progress can be updated in the ProgressChanged event handler.
MSDN Reference
Can any one tel me what is wrong in the code below and how to fix it?
The problem is here
public void testTaskUpdateLabel(string labelTeks)
{
Task taskUpdateLabel = new Task(() =>
{
label4.Text = labelTeks;
});
taskUpdateLabel.Start(uiScheduler);
}
You should not use TPL to update the UI. TPL tasks are for doing non UI work and UI should only be updated on the UI thread. You already moved the work on a thread pool thread (via Task.Run), so the only problem you need to solve is how to update the UI from inside the worker. There are many ways to do that - using Control.Invoke/BeginInvoke, SynchronizationContext etc, but the preferred approach for TPL is to pass and use IProgress<T> interface. Don't be fooled by the name - the interface is an abstraction of a callback with some data. There is a standard BCL provided implementation - Progress<T> class with the following behavior, according to the documentation
Any handler provided to the constructor or event handlers registered with the ProgressChanged event are invoked through a SynchronizationContext instance captured when the instance is constructed.
i.e. perfectly fits in UI update scenarios.
With all that being said, here is how you can apply that to your code. We'll use IProgress<string> and will call Report method and pass the full name for each file/directory we find - a direct replacement of your testTaskUpdateLabel calls.
private void button1_Click(object sender, EventArgs e)
{
var progress = new Progress<string>(text => label4.Text = text);
Task.Run(() =>
{
WalkDirectory(new DirectoryInfo(drive), progress);
});
}
public bool WalkDirectory(DirectoryInfo directory, IProgress<string> progress)
{
if (directory == null) throw new ArgumentNullException("directory");
if (progress == null) throw new ArgumentNullException("progress");
return WalkDirectories(directory, progress);
}
bool WalkDirectories(DirectoryInfo directory, IProgress<string> progress)
{
// ...
if (!(continueScan = WalkDirectories(subDirectory, progress)))
// ...
if (continueScan)
progress.Report(directory.FullName);
// ...
}
bool WalkFilesInDirectory(DirectoryInfo directory, IProgress<string> progress)
{
// ...
try
{
progress.Report(file.FullName);
}
// ...
}
I got it to work by making the walkDirectory, walkDirectories and WalkFiles methods async. Thus using the await keyword before I call the testUpdate and testUpdateLabel methods. This way the listview is updated with the search results while the search is running without blocking the UI thread. I.E. the user can cancel the search when the file he was searching for has been found.
When I tried to call MahApps Metro Dialog Boxes I am getting error while Passing Values
while calling Dialog Control when Passing parameters I need to pass Metrowindow parameter
But I need to call it in User control
Below is the Method I will call when I need Dialog control
public async void ShowMessageDialog(object sender, RoutedEventArgs e)
{
// This demo runs on .Net 4.0, but we're using the Microsoft.Bcl.Async package so we have async/await support
// The package is only used by the demo and not a dependency of the library!
var mySettings = new MetroDialogSettings()
{
AffirmativeButtonText = "Hi",
NegativeButtonText = "Go away!",
FirstAuxiliaryButtonText = "Cancel",
// ColorScheme = MetroDialogOptions.ColorScheme
};
MessageDialogResult result = await this.ShowMessageAsync("Hello!", "Welcome to the world of metro!",
MessageDialogStyle.AffirmativeAndNegativeAndSingleAuxiliary, mySettings);
if (result != MessageDialogResult.FirstAuxiliary)
await this.ShowMessageAsync("Result", "You said: " + (result == MessageDialogResult.Affirmative ? mySettings.AffirmativeButtonText : mySettings.NegativeButtonText +
Environment.NewLine + Environment.NewLine + "This dialog will follow the Use Accent setting."));
}
public static Task<MessageDialogResult> ShowMessageAsync(this MetroWindow window, string title, string message, MessageDialogStyle style = MessageDialogStyle.Affirmative, MetroDialogSettings settings = null)
{
window.Dispatcher.VerifyAccess();
return HandleOverlayOnShow(settings, window).ContinueWith(z =>
{
return (Task<MessageDialogResult>)window.Dispatcher.Invoke(new Func<Task<MessageDialogResult>>(() =>
{
if (settings == null)
{
settings = window.MetroDialogOptions;
}
//create the dialog control
var dialog = new MessageDialog(window, settings)
{
Message = message,
Title = title,
ButtonStyle = style
};
SizeChangedEventHandler sizeHandler = SetupAndOpenDialog(window, dialog);
dialog.SizeChangedHandler = sizeHandler;
return dialog.WaitForLoadAsync().ContinueWith(x =>
{
if (DialogOpened != null)
{
window.Dispatcher.BeginInvoke(new Action(() => DialogOpened(window, new DialogStateChangedEventArgs())));
}
return dialog.WaitForButtonPressAsync().ContinueWith(y =>
{
//once a button as been clicked, begin removing the dialog.
dialog.OnClose();
if (DialogClosed != null)
{
window.Dispatcher.BeginInvoke(new Action(() => DialogClosed(window, new DialogStateChangedEventArgs())));
}
Task closingTask = (Task)window.Dispatcher.Invoke(new Func<Task>(() => dialog._WaitForCloseAsync()));
return closingTask.ContinueWith(a =>
{
return ((Task)window.Dispatcher.Invoke(new Func<Task>(() =>
{
window.SizeChanged -= sizeHandler;
window.RemoveDialog(dialog);
return HandleOverlayOnHide(settings, window);
}))).ContinueWith(y3 => y).Unwrap();
});
}).Unwrap();
}).Unwrap().Unwrap();
}));
}).Unwrap();
}
ShowMessageAsync is an extension method for MetroWindow, so this code should work:
var metroWindow = (Application.Current.MainWindow as MetroWindow);
await metroWindow.ShowMessageAsync(title, message);
Let me give you a reallife example based on Rajesh answer.
async void LoadData()
{
var metroWindow = (Application.Current.MainWindow as MetroWindow);
var controller = await metroWindow.ShowProgressAsync("Procesando", "Obtener datos de la base de datos",
false, new MetroDialogSettings() { AnimateShow = true, ColorScheme = MetroDialogColorScheme.Theme});
controller.SetIndeterminate();
await viewModel.LoadData();
await Dispatcher.BeginInvoke((Action)(async () =>
{
DataGrid1.ItemsSource = viewModel.AModels;
await controller.CloseAsync();
}));
}
#Rajesh code does null exception for me inside usercontrol. Even though my MainWindow is a MetroWindow class. However, the below worked for my configuration;
#region try show Message
try
{
#region ok, lets show message
foreach (System.Windows.Window window in System.Windows.Application.Current.Windows)
{
if (window.GetType() == typeof(MainWindow))
{
var controller = await (window as MainWindow).ShowProgressAsync("My Title", "My long message content text.",
false, new MetroDialogSettings() { AnimateShow = true, ColorScheme = MetroDialogColorScheme.Theme });
}
}
#endregion ok, lets show message
}
catch (Exception ex)
{
#region error block
#endregion error block
}
#endregion try show Message
This is a another option because i was try with Rajesh Akshith answer but it is not work for me.
In usercontrol,
using MahApps.Metro.Controls.Dialogs;
private IDialogCoordinator dialogCoordinator;
public async Task ShowMessageAsync()
{
dialogCoordinator = DialogCoordinator.Instance;
await dialogCoordinator.ShowMessageAsync(this,"Header","Body");
}
I was reference from Mahapps Dialog .I think this was more useful and It also can write in helper class and you can call this from anywhere.
In my MainWindow:
public async void ShowMessagesDialog(string title, string message)
{
logger.Add("ShowMessagesDialog: " + message);
var messageDialogSettings = new MetroDialogSettings
{
AffirmativeButtonText = "Aceptar",
OwnerCanCloseWithDialog = false,
ColorScheme = this.MetroDialogOptions.ColorScheme,
};
await this.ShowMessageAsync(title, message, MessageDialogStyle.Affirmative, messageDialogSettings);
}
In my UserControl:
(Application.Current.MainWindow as MainWindow).ShowMessagesDialog("Información", "No ha seleccionado un archivo");
In my MainViewModel I have only RelayCommands that open different pages.
Theese commands are like this
Messenger.Default.Send<int>(2015);
ViewModel.ReportViewModel reportVM = new ReportViewModel(report);
Views.ReportView pagReport = new ReportView() { DataContext = reportVM };
ApplicationHelper.NavigationService(pagReport);
in ReportViewModel I have
public ReportViewModel(string report)
{
Messenger.Default.Register<int>(this, Doit);
ShowReport(report);
}
private void Doit(int val)
{
int test = val;//code never touch this line
}
What I'm doing wrong ?
Per my comment above, you're instantiating/newing up reportVM after you send the message. If there isn't an instance of ReportViewModel before the message is sent then that message has no listener.
//Instantiate first:
ViewModel.ReportViewModel reportVM = new ReportViewModel(report);
Views.ReportView pagReport = new ReportView() { DataContext = reportVM };
ApplicationHelper.NavigationService(pagReport);
//Send the message:
Messenger.Default.Send<int>(2015);
I am having a problem with getting data from db and showing in UI asynchronously.
I am using MVVM light, when I click the button, action is triggered in ViewModel:
private void SearchQuery(string query)
{
_redisModel.GetFriendsListAsync(query);
}
At some point GetFriendsListCompleted is called by background thread notifing viewmodel that job is done.
At this point I need to update ListBox ItemSource. But when I try to update is I get
“The calling thread cannot access this object because a different thread owns it”
I have tried Dispatcher.CurrentDispatcher.Invoke(),App.Current.Dispatcher.Invoke() and different magic, but it still doesn’t work.
I tried to give UI dispatcher to ViewModel and then call it from there - didn't work.
private string filterText = string.Empty;
public string FilterText
{
get { return filterText; }
set
{
filterText = value;
this.RaisePropertyChanged(() => this.FilterText);
this.FriendsList.View.Refresh(); // Here where exception is happening.
}
}
I tried to change this line to
Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
() =>this.FriendsList.View.Refresh())); - still the same.
I am using Telerik ListBox to display items. FriendList is CollectionViewSource(http://www.telerik.com/help/wpf/radlistbox-overview.html). It works when I use Telerik example from WPF Control Examples. Problems start to occur when I use my async methods.
Type of view is System.ComponentModel.ICollectionView it is used for Filtering and Grouping.
I have also tried to just assign ObservableCollection to Items property of the ListBox and it doesn't work either.
A bit more details on how _redisModel.GetFriendsListAsync works:
In the end(after all chain of calls) it ends up here:
public GetAsyncResult(Func<T> workToBeDone, Action<IAsyncResult> cbMethod, Object state)
{
_cbMethod = cbMethod;
_state = state;
QueueWorkOnThreadPool(workToBeDone);
}
ThreadPool.QueueUserWorkItem(state =>
{
try
{
_result = workToBeDone();
}
catch (Exception ex)
{
_exception = ex;
}
finally
{
UpdateStatusToComplete(); //1 and 2
NotifyCallbackWhenAvailable(); //3 callback invocation
}
});
In viewmodel I have method:
private void GetFriendsListCompleted(object sender, ResultsArgs<Friend> e)
{
if (!e.HasError)
{
var curr = e.Results;
if (curr != null)
{
this.FriendsList= new CollectionViewSource();
this.FriendsList.Source = list;
this.FriendsList.Filter += this.FriendFilter;
FilterText = "";
Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
() => this.FriendsList.View.Refresh()));
}
}
Can anybody please help me with this ?
Thank you
You are creating CollectionViewSource in one thread and refreshing that in another thread (dispatcher thread). Update your GetFriendsListCompleted to
private void GetFriendsListCompleted(object sender, ResultsArgs<Friend> e)
{
if (!e.HasError)
{
var curr = e.Results;
if (curr != null)
{
Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
() => {
this.FriendsList= new CollectionViewSource();
this.FriendsList.Source = list;
this.FriendsList.Filter += this.FriendFilter;
FilterText = "";
this.FriendsList.View.Refresh();
}));
}
}
}
You haven't shown any of the code that's actually running on the background thread on completion but I'm guessing that in it you're creating a collection object that you're then trying to assign to your CollectionView. When the CV tries to update (on the UI thread) from your Refresh call it would then try to use the collection that's owned by the other thread.
If you include the relevant code it would be easier to say for sure.
I am working on a 'proof of concept' Silverlight 4 project and am learning the way of THE ASYNC. I have stopped fighting the urge to implement some pseudo-synchronous smoke and mirrors technique. I am going to learn to stop worrying and love THE ASYNC.
Most of the time I just use a BusyIndicator while async methods are running and all is good but I have run into a few situations where I need to call methods sequentially. I put together this example and it works. But in my experience... if it works... there is something wrong with it.
When is this going to blow up in my face or steal my wife or date one of my daughters?
Is there a better way to do this?
The Code:
public class CustomPage : Page
{
static readonly object _AsyncMethodChain_Lock = new object();
private Dictionary<Action<object>, string> _AsyncMethodChain = new Dictionary<Action<object>, string>();
public Dictionary<Action<object>, string> AsyncMethodChain
{
get { lock (_AsyncMethodChain_Lock) { return this._AsyncMethodChain; } }
set { lock (_AsyncMethodChain_Lock) { this._AsyncMethodChain = value; } }
}
private void CustomPage_Loaded(object sender, RoutedEventArgs e)
{
if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
{
var user = this.SecurityProvider.UserObject as TimeKeeper.UserServiceReference.User;
if (user == null)
return;
this.AsyncMethodChain.Add(
data =>
{
var userServiceClient = new UserServiceClient();
userServiceClient.GetCompleted +=
(send, arg) =>
{
var userViewSource = this.Resources["userViewSource"] as CollectionViewSource;
userViewSource.Source = new List<UserServiceReference.User>(new UserServiceReference.User[1] { arg.Result });
userViewSource.View.MoveCurrentToPosition(0);
this.AsyncMethodChain.ExecuteNext(arg.Result.UserID, this.BusyIndicator);
};
userServiceClient.GetAsync(user.UserID);
},
"Loading user..."
);
this.AsyncMethodChain.Add(
data =>
{
var userID = (int)data;
var timeLogServiceClient = new TimeLogServiceClient();
timeLogServiceClient.FindByUserIDCompleted +=
(send, arg) =>
{
var timeLogViewSource = this.Resources["timeLogViewSource"] as CollectionViewSource;
timeLogViewSource.Source = arg.Result;
this.AsyncMethodChain.ExecuteNext(null, this.BusyIndicator);
};
timeLogServiceClient.FindByUserIDAsync(userID);
},
"Loading time logs..."
);
this.AsyncMethodChain.ExecuteNext(null, this.BusyIndicator);
}
}
}
public static class Extensions
{
public static void ExecuteNext(this Dictionary<Action<object>, string> methods, object data, BusyIndicator busyIndicator)
{
if (methods.Count <= 0)
{
busyIndicator.BusyContent = "";
busyIndicator.IsBusy = false;
return;
}
else
{
var method = methods.Keys.ToList<Action<object>>()[0];
busyIndicator.BusyContent = methods[method];
busyIndicator.IsBusy = true;
}
methods.ExecuteNext(data);
}
public static void ExecuteNext(this Dictionary<Action<object>, string> methods, object data)
{
var method = methods.Keys.ToList<Action<object>>()[0];
methods.Remove(method);
method(data);
}
}
What you have sine looks pretty good, but if you are still worried about the callsequence i would sugest that you create a new method in your webservice which will call the other four in whatever sequence you need them to and call this new method from your silverlight application.
I dont see the need for you to do that as your current implemenation is pretty good as well.