So, I am fairly new to MVVM and have backed myself into an interesting corner where I am not sure how to make things work with either a behavior or a command. I have a user control that contains a listbox of items which need to implement various behaviors such as deleting or removing the given item. Like so:
<UserControl> // DataContext is a viewmodel
// Borders, grids, various nesting controls...
<ListBox x:Name="ListBox_Items" ItemSource="{Binding ItemsList}">
<ListBox.ItemTemplate>
<DataTemplate> // From here on the individual item has its own data context of type Item in ItemsList
<StackPanel Orientation="Horizontal">
<TextBox Name="EditItemStuffOnLoseFocus" Text="{Binding ItemStuff}"/>
<Button Name="DeleteItemStuff"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
The example has been simplified, but the basic idea is that the textbox should edit its associated listbox item when it loses focus and the button should delete the associated listbox item when pressed. At first I implemented commands for this and had both working, until I realized that I had forgotten the standard "Are you sure?" message. I added this in the command, but since it has no concept of the actual objects, I can't think of how to tell it where to put the dialog window. The command accepts a view model (_ViewModel) on creation and accepts the Item model (textbox/button's DataContext) as a parameter. With the basic message box dialog, the Execute() method looked something like this (simplified):
public void Execute(object parameter)
{
if (MessageBox.Show("Really delete the item?", "Delete Item", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
ItemService service = new ItemService();
service.RemoveItem(((Item)parameter).ItemID);
if (_ViewModel.ReloadItemListCommand.CanExecute(_ViewModel.ItemInfo))
_ViewModel.ReloadItemListCommand(_ViewModel.ItemInfo);
}
}
Of course, this message box is not centered on the application, which is small but annoying. A coworker suggested that I replace the Command with a Behavior so that I would have an associated object to use for centering the message box. The problem is, I haven't been able to find any information on passing parameters to a behavior, or how to trace back multiple levels from an associated object to its parents so that I can get the view model for the reloading step as well as the individual item's model (the associated object's DataContext).
In summary, is there a way to either center the MessageBox on the application within the command while remaining MVVM-friendly, OR to pass parameters / retrieve a specific parent object or its resources using a behavior?
____________ UPDATE ____________
The answer below works great, but I went another route so that I could use DataContext variables in my MessageBox. I managed to preserve access to the DataContext of the calling control and the view model by adding the view model to the control's tag:
<UserControl> // DataContext is a viewmodel
// Borders, grids, various nesting controls...
<ListBox x:Name="ListBox_Items" ItemSource="{Binding ItemsList}">
<ListBox.ItemTemplate>
<DataTemplate> // From here on the individual item has its own data context of type Item in ItemsList
<StackPanel Orientation="Horizontal">
<TextBox Name="EditItemStuffOnLoseFocus" Text="{Binding ItemStuff}" Tag={Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=DataContext}"/>
<Button Name="DeleteItemStuff" Tag={Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=DataContext}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
I'm not entirely certain this is the best way to be using Tag, but it does get all the information I need into the behavior while allowing me to center the MessageBox. The behavior is similar to the command except for a few added lines to extract the model and view model. Keeping with the initial shortened example, it looks something like this:
ExampleViewModel viewModel = (ExampleViewModel)AssociatedObject.Tag;
Item parameter = (Item)AssociatedObject.DataContext;
if (MessageBox.Show(Window.GetWindow(AssociatedObject), "Really delete the item?", "Delete Item", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
{
ItemService service = new ItemService();
service.RemoveItem(((Item)parameter).ItemID);
if (viewModel.ReloadItemListCommand.CanExecute(viewModel.ItemInfo))
viewModel.ReloadItemListCommand(viewModel.ItemInfo);
}
}
Thank you all for the help.
To center a message box on a window will require you to either implement your own Window and do a ShowDialog where you can specify the location. Or you can inherit from the Forms control done in this CodeProject Solultion.
However for the first part of your problem It would most likely be easier to implement a click handler on the button and bind the delete to your user control as a dependency property. This would allow you to have access to the sender and keep the UI compeltely inside the control.
xaml
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Label Content="{Binding Name}" />
<Button Click="Button_Click" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Code Behind
public ICommand DeleteItem
{
get { return (ICommand)GetValue(DeleteItemProperty); }
set { SetValue(DeleteItemProperty, value); }
}
public static readonly DependencyProperty DeleteItemProperty = DependencyProperty.Register("DeleteItem", typeof(ICommand), typeof(control), new PropertyMetadata(null));
private void Button_Click(object sender, RoutedEventArgs e)
{
if (DeleteItem != null)
{
var result = System.Windows.MessageBox.Show("WOULD YOU LIKE TO DELETE?", "Delete", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
{
var fe = sender as FrameworkElement;
if (DeleteItem.CanExecute(fe.DataContext))
{
DeleteItem.Execute(fe.DataContext);
}
}
}
Just have your delete command bind from the outside and do the logic for your message box in the click event.
Related
Base Problem => my ListBox won't update when a different ComboBox value is selected
I'm making a WPF application following the MVVM pattern (or at least trying to). I have a list of servers that I fetch based on the currently selected
"Application". The list of servers are put into a ListBox, and I want the ListBox to update with the new servers when I changed the "Application" via a dropdown menu.
The "Current:" value will change based on the selection made (so that binding at least works). I have a ServerListViewModel class which implements INotifyPropertyChanged. Here's a snippet of it
ServerListViewModel (snippet)
public ServerListViewModel()
{
_serverListModel = new ServerListModel
{
ServerList = ConfigUtility.GetServers()
};
}
...
public BindingList<string> ServerList
{
get { return _serverListModel.ServerList; }
set
{
if (ReferenceEquals(_serverListModel.ServerList, value)) return;
_serverListModel.ServerList = value;
InvokePropertyChanged("ServerList");
}
}
The constructor properly works properly and the ListBox updates to reflect the ServerList property. 'ConfigUtility.GetServers' uses a saved value of the active "Application" in a JSON file.
In this class I setup a static property like this so that I could try to access this class from another view model
public static ServerListViewModel Instance { get; } = new ServerListViewModel();
SettingsViewModel.cs
The dropdown menu is on a settings tab, while the server list has its own tab. These tabs have their own view models.
Here's a snippet of this view model:
public ComboBoxItem CurrentApplication
{
get { return _settingsModel.CurrentApplication; }
set {
SetCurrentApplication(value);
}
}
private void SetCurrentApplication(ComboBoxItem value)
{
if (ReferenceEquals(_settingsModel.CurrentApplication, value)) return;
_savedSettings = MySettings.Load();
if (value.Content == null)
{
_settingsModel.CurrentApplication =
new ComboBoxItem { Content = _savedSettings.CurrentApplication };
}
else
{
_settingsModel.CurrentApplication = value;
_savedSettings.CurrentApplication = (string)value.Content;
_savedSettings.Save();
ServerListViewModel.Instance.ServerList = ConfigUtility.GetServers();
}
InvokePropertyChanged("CurrentApplication");
}
I also have a MySettings object which saves values to a JSON file so that I can save the active "Application" b/w sessions.
XAML
<TabControl DockPanel.Dock="Bottom" HorizontalAlignment="Stretch" Height="Auto" TabStripPlacement="Bottom">
<!-- Server List -->
<TabItem Name="ServerListTab" Header="Server List">
<TabItem.DataContext>
<viewModel:ServerListViewModel />
</TabItem.DataContext>
<ListBox HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding ServerList, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="True"
SelectedItem="{Binding SelectedServer}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="FontSize" Value="14"></Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</TabItem>
<!-- Settings -->
<TabItem Name="SettingsTab" Header="Settings">
<TabItem.DataContext>
<viewModel:SettingsViewModel />
</TabItem.DataContext>
<StackPanel>
<TextBlock FontWeight="Bold">Application</TextBlock>
<WrapPanel>
<TextBlock>Current:</TextBlock>
<TextBlock Text="{Binding CurrentApplication.Content}"></TextBlock>
</WrapPanel>
<TextBlock>Select</TextBlock>
<ComboBox Name="TheComboBox" SelectedItem="{Binding CurrentApplication}">
<ComboBoxItem>NetWebServer</ComboBoxItem>
<ComboBoxItem>NetSearchService</ComboBoxItem>
<ComboBoxItem>NetInterface</ComboBoxItem>
</ComboBox>
<TextBlock FontWeight="Bold">Service</TextBlock>
<WrapPanel>
<TextBlock>Current:</TextBlock>
<TextBlock></TextBlock>
</WrapPanel>
<TextBlock>Select</TextBlock>
<ComboBox></ComboBox>
<CheckBox>
Perfomance mode
</CheckBox>
<CheckBox>
Show live servers
</CheckBox>
<CheckBox>
Show test servers
</CheckBox>
<CheckBox>
Show only parameters with IP's
</CheckBox>
</StackPanel>
</TabItem>
</TabControl>
I haven't separate these into separate views yet. Is the problem they are not separate user controls in different files with their own view models? I have set the different tab items to the corresponding view model however.
Conclusion/Question
How can I get it so that by selecting a new item in the ComboBox, it also updates the server list? By debugging my code, it seems everything works line by line so somehow the ListBox must not be notified of a change? If I close the app with a different "Application" selected, then open it back up, it correctly uses the saved setting to populate the ListBox with the new values. But while the app is open I cannot get the ListBox to change. Any ideas?
In your XAML, you create a ServerListViewModel object, which is being set as DataContext for a TabItem.
However, in the method SetCurrentApplication you update the ServerList property of another ServerListViewModel object (the initializer of this static property created it). You don't see any change in the UI because the ServerListViewModel object used as DataContext has actually not been touched.
Simply use the ServerListViewModel object from the static ServerListViewModel.Instance property as your DataContext:
<TabItem
DataContext="{x:Static viewModel:ServerListViewModel.Instance}"
Name="SettingsTab" Header="Settings"
>
...
Now, updating ServerListViewModel.Instance.ServerList within the SetCurrentApplication method should cause the data bindings to update.
Another, cleaner alternative is to eschew that static Instance property and use some kind of messaging between the view-models. The SetCurrentApplication method could send a message that the server list should be updated. The ServerListViewModel class would process that message and do the actual work of updating the server list. MVVM libraries such as MVVM Light and others provide Messenger components which facilitate this.
If I follow everything, SetCurrentApplication() would need to refresh the server list and pop an INotifyChanged event. It doesn't look like its doing that.
I want to program a dynamic Detail View. Like a user clicked on an Item then he sees the detail view. Now he sees all Values, but when he only want to see a few values, he click on a config Button in this view and a second view opens where he can select and unselect all types of values. Like he dont want to see the Description, the he deselect it in the second view, and it´s no longer visible in the first view.
The only way for me to implement something like this is to programm a Function which generates the first view. The view would be a UI-Element. Which is then returned to the Windows where the UI-Element is set a child of an Element on the Window. But I think this isn´t a good way. How do you would solve this problem?
Thanks for every hint :)
If I understand well you want
List -> Details -> MoreDetails/Edit
Depending on what platform you are creating is a bit different but the idea is the following:
<ItemsControl ... x:Name="ItemsList" ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Tapped">
<core:InvokeCommandAction
Command="{Binding Datacontext.ShowItemDetails, ElementName=ItemsList}" CommandParameter="{Binding}"/>
</core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Grid>
</DataTemplate>
</ItemsControl>
Now when you tap the grid you can show a PopupControl with the details and the DataContext:
public Command<ItemClass> ShowPopup
{
get
{
return new Command<ItemClass>((i)=>
{
//Create the Popup
});
}
}
In case you are not using MVVM you can add the Command in code behind and place in the page contructor this.DataContext = this; and place the previous command there.
And now create a control for the content of the popup, bind the properties to the item details, now add another behavior with a command in that control and unhide the controls for edit mode or more details mode
You could have for each detail item a property bool ItemXIsVisible which is bound to a checkbox in the config view, and to the IsVisible property of the X control in the detail view?
My name is Andrea this is my first post ever.
Frequently you have helped me as a simple reader, now I'm writing because I wanted to direct support.
I have to create and a tab control and with a button "Add Tab" I have to add a new tab with the same content.
Up to this everything is fine.
Within Tab I have a textedit and a combobox.
My problems are two:
1 How do I load the contents of the combobox for each tab I add?
2 Every time I write the text of and a edit tab override also edit the text of the other tab.
Here the code:
Data Template in Xaml:
<DataTemplate x:Key="tabItemContent">
<dxlc:LayoutGroup Orientation="Vertical" Header="Target Description" IsCollapsible="True">
<!--Name-->
<dxlc:LayoutItem>
<dxlc:LayoutGroup Orientation="Horizontal" ItemSpace="4" >
<dxlc:LayoutItem Label="Name" Margin="10">
<dxe:TextEdit x:Name="TextEdit_NameTarget"/>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
</dxlc:LayoutItem>
<!--Nation e Label-->
<dxlc:LayoutItem>
<dxlc:LayoutGroup Orientation="Horizontal" ItemSpace="12" >
<dxlc:LayoutItem Label="Nation" Margin="10">
<ComboBox x:Name="ComboBox_TargetNazione" />
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
</dxlc:LayoutItem>
</dxlc:LayoutGroup>
</DataTemplate>
C#:
private void Button_Click_Add(object sender, RoutedEventArgs e)
{
DataTemplate tabItemDataTemplate = this.TryFindResource("tabItemContent") as DataTemplate;
DXTabItem tabItem = new DXTabItem();
tabItem.Header = "New Tab";
tabItem.ContentTemplate = tabItemDataTemplate;
tabControl_Targets.Items.Add(tabItem);
}
Here's where to load the list into the combobox:
private void LoadComboBoxNation()
{
ComboBox_TargetNazione.ItemsSource =
ManagementTriple.Istance().get_Nation_byTipologyAndContext(ComboBox_TypologyScenario.SelectedItem.ToString(),
ComboBox_ContextScenario.SelectedItem.ToString());
controlloselecteditem(ComboBox_SourceNazione.SelectedItem.ToString());
controlloselecteditem(ComboBox_TargetNazione.SelectedItem.ToString());
}
Thank you all for the help that you can give me.
DataTemplates require a simple but fundamental requirement to work properly: you should use the ViewModel-First approach.
Ideally, your tab control should have a Binding to some ViewModel. Then, if you want another tab to appear, you should use your button click to call a Command in your ViewModel, then the ViewModel would add another item to your TabControl ItemsSource property (which would be some collection), and the new item would be displayed "automagically" with its respective DataTemplate.
The idea of WPF is to replace all this imperative code in the View (like the one you posted) with a more indirect one, where your only worry is to manipulate things in the ViewModel, and the "Dumb View" just follows.
Hope this helps, but don't hesitate to ask for additional details in the comments.
I'm adding a close button to my tabs using the following guide:
http://www.codeproject.com/Articles/84213/How-to-add-a-Close-button-to-a-WPF-TabItem
This has become a problem because the event uses the 'parent' of the added tab to remove that tab from the tabcontrol. I'm binding the tab control using mvvm, so the parent property is apparently not being set and giving me a null reference exception for the parent when the event tries to remove from it.
Here's the binding so you get the idea:
<TabControl Name="tabControl" Margin="0,22,0.2,-5.2" ItemsSource="{Binding Tabs}" Background="#FF4C76B2"/>
Heres where the tabs are being added.
private void AddTab(object tabName)
{
ClosableTab newTab = new ClosableTab();
newTab.Title = "title?";
//newTab.Header = tabName;
TextBox test = new TextBox();
test.Text = "CONTENT (" + tabName + ") GOES HERE";
newTab.Content = test;
Tabs.Add(newTab);
OnPropertyChanged("Tabs");
}
Here is the event where the null reference is taking place:
void button_close_Click(object sender, RoutedEventArgs e)
{
((TabControl)this.Parent).Items.Remove(this);
}
As I see it there are two options:
try to find another way to remove the tab (without the parent
property)
try to find a way to somehow set the parent property (which cant be
done directly, it throws a compiler error)
That doesn't sound like MVVM to me. We work with data, not UI elements. We work with collections of classes that contain all of the properties required to fulfil some requirement and data bind those properties to the UI controls in DataTemplates. In this way, we add UI controls by adding data items into these collections and let the wonderful WPF templating system take care of the UI.
For example, you have a TabControl that we want to add or remove TabItems from... in a proper MVVM way. First, we need a collection of items that can represent each TabItem:
public static DependencyProperty ItemsProperty = DependencyProperty.Register("Items", typeof(ObservableCollection<string>), typeof(TestView));
public ObservableCollection<string> Items
{
get { return (ObservableCollection<string>)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
I'm just using a DependencyProperty because I knocked this up in a UserControl and I'm just using a collection of strings for simplicity. You'll need to create a class that contains all of the data required for the whole TabItem content. Next, let's see the TabControl:
<TabControl ItemsSource="{Binding Items}" ItemTemplate="{StaticResource ItemTemplate}" />
We data bind the collection to the TabControl.ItemsSource property and we set the TabControl.ItemTemplate to a Resource named ItemTemplate. Let's see that now:
xmlns:System="clr-namespace:System;assembly=mscorlib"
...
<DataTemplate x:Key="ItemTemplate" DataType="{x:Type System:String}">
<TabItem Header="{Binding}" />
</DataTemplate>
This DataTemplate defines what each item in our collection will look like. For simplicity's sake, our strings are just data bound to the TabItem.Header property. This means that for each item we add into the collection, we'll now get a new TabItem with its Header property set to the value of the string:
Items.Add("Tab 1");
Items.Add("Tab 2");
Items.Add("Tab 3");
Note that I included the System XML Namespace Prefix for completeness, but you won't need that because your DataType will be your own custom class. You'll need more DataTemplates too. For example, if your custom class had a Header property and a Content property, which was another custom class, let's say called Content, that contained all of the properties for the TabItem.Content property, you could do this:
<DataTemplate x:Key="ItemTemplate" DataType="{x:Type YourPrefix:YourClass}">
<TabItem Header="{Binding Header}" Content="{Binding Content}" />
</DataTemplate>
<DataTemplate DataType="{x:Type YourPrefix:Content}">
<YourPrefix:SomeUserControl DataContext="{Binding}" />
</DataTemplate>
So this would give you TabItems with Headers set and Content that comes from SomeUserControl which you could design. You don't need to use UserControls, you could just add more UI controls to either DataTemplate. But you will need to add more controls somewhere... and more classes and properties, always remembering to correctly implement the essential INotifyPropertyChanged interface.
And finally, to answer your question in the proper MVVM way... to remove a TabItem, you simply remove the item that relates to that TabItem from the collection. Simple... or it would have been if you really had been using MVVM like you claim. It's really worth learning MVVM properly as you'll soon see the benefits. I'll leave you to find your own tutorials as there are many to chose from.
UPDATE >>>
Your event handling is still not so MVVM... you don't need to pass a reference of any view model anywhere. The MVVM way is to use commands in the view model. In particular, you should investigate the RelayCommand. I have my own version, but these commands enable us to perform actions from data bound Buttons and other UI controls using methods or inline delegates in the view model (where action and canExecute in this example are the CommandParameter values):
<Button Content="Close Tab" Command="{Binding CloseTabCommand}"
CommandParameter="{Binding}" />
...
public ICommand CloseTabCommand
{
get { return new ActionCommand(action => Items.Remove(action),
canExecute => canExecute != null && Items.Contains(canExecute)); }
}
So whatever view model has your Tabs collection should have an AddTabCommand and a CloseTabCommand that add and remove items from the Tabs collection. But just to be clear, for this to work properly, your ClosableTab class should be a data class and not a UI control class. Use a DataTemplate to specify it if it is a UI control.
You can find out about the RelayCommand from this article on MSDN.
I have a Grid on a wpf window which I want to add the capability that user can delete some of the items by clicking on a delete button. The application uses Calibrun Micro to bind view to ViewModel.
My question?
1- Is it a good idea to use a button to delete an item from a grid in WPF?
2- How can I bind a button to a method on VM and in the methd get a pointer to the item that should be deleted?
Edit1
I added the buttons in this way to datagrid:
<DataGridTemplateColumn Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Delete" cal:Message.Attach="DeleteFromList($dataContext)" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
and c# code follow:
public void DeleteFromList(object tmp)
{
}
But the buttons on datagrid are disabled and clicking on them doesn't fire DeleteFromList method (I checked using debugger).
Why they are disabled? How can I make them enabled?
This depends on how your button is placed - is there a single 'delete' button or have you added a button per row in the grid (are we talking DataGrid or just Grid?)
Assuming you are talking about DataGrid, you can easily just add an action message command to the button and pass through the item which is being deleted to the message handler on the VM
e.g. in the VM
public class MyViewModel
{
public DataItemCollectionTypeName ItemCollection { get; set; }
public void DeleteItem(DataItemTypeName item)
{
ItemCollection.Remove(item);
}
}
Assuming ItemCollection is bound to the grid, the button XAML may look like this:
<Button cal:Message.Attach="[Click] = [DeleteItem($datacontext)]" />
You may also need to set Action.TargetWithoutContext (it should be bound to the VM) if this is a templated row, as otherwise CM will not be able to locate the VM to invoke the action message on
If you have a single button that isn't contained within the grid you can always target the grids SelectedItem in the action message
<DataGrid x:Name="SomeDataGrid"></DataGrid>
<Button cal:Message.Attach="[Click] = [DeleteItem(SomeDataGrid.SelectedItem)]" />
It may be (and probably is) the default property that CM will look at so you may not need to specify the property name unless you have modified default conventions
<DataGrid x:Name="SomeDataGrid"></DataGrid>
<Button cal:Message.Attach="[Click] = [DeleteItem(SomeDataGrid)]" />
Edit
To clarify: In order for CM to find a VM to call the DeleteItem method it uses the DataContext of the current item. In the case of an ItemsControl derived control, the datacontext for each item points to the item being bound, not the ViewModel.
In order to give CM a hint as to which object it should try to resolve the DeleteItem method on, you can use the Action.TargetWithoutContext attached property, which applies a target object for action messages without changing the DataContext of the bound row/item
You can use element name syntax to point to the correct place:
In this example I've used a grid as the root element and named it LayoutRoot, then I've pointed the action message target to LayoutRoot.DataContext (which will be the ViewModel) using ElementName syntax. You can use any method (AncestorType or whatever)
<Grid x:Name="LayoutRoot">
<DataGridTemplateColumn Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Delete" cal:Message.Attach="DeleteFromList($dataContext)" cal:Action.TargetWithoutContext="{Binding DataContext, ElementName=LayoutRoot}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</Grid>
That should then work!
You could do something like this...
<Button cal:Message.Attach="[Event MouseEnter] = [Action Save($this)]">
Check the docs as they will explain what you need to do and should answer your question: link