Update UserControl when parent item is changed - c#

What is the proper way (MVVM) to handle following situation? We have an window/user control which hosts few user controls and grid. When we select grid item, SelectedItem="{Binding SelectedAccount, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" is updating SelectedAccount property on user controls
<TabItem Header="{x:Static p:Resources.Basic}">
<DockPanel>
<accounts:UCBasic x:Name="UCBasic" SelectedAccount="{Binding SelectedItem, ElementName=gridMain}"></accounts:UCBasic>
</DockPanel>
</TabItem>
<TabItem Header="{x:Static p:Resources.AdditionalData}">
<DockPanel>
<accounts:UCAdditionalData x:Name="UCAdditionalData" SelectedAccount="{Binding SelectedItem, ElementName=gridMain}"></accounts:UCAdditionalData >
</DockPanel>
... more user controls ...
</TabItem>
using their DependencyProperty. Now, how would I write PageModel for above user controls (UCBasic, UCAdditionalData) so they can load/show more data depending on SelectedAccount from grid. There is dirty way of using property changed event but I don't think it should be done that way. Each user control has this:
public Account SelectedAccount
{
get { return (Account)GetValue(SelectedAccountProp); }
set
{
SetValue(SelectedAccountProp, value);
}
}
public static readonly DependencyProperty SelectedAccountProp = DependencyProperty.Register("SelectedAccount", typeof(Account), typeof(UCBasic));
Essentialy, how I would notify this user control that SelectedAccount value is changed and it should update itself (its own textboxes, grids and so on)?

if each user control has Account property, it can do bindings in its own textboxes, grids and so on, e.g.
<TextBox Text="{Binding Account.Name, RelativeSource={RelativeSource AncestorType=UserControl}}"/>

1) you can use INotifyPropertyChanged implementation in your ViewModel (if will send notification to update changed ViewModel property on View)
2) If you use one ViewModel for both user controls 1 option should help you immediately.
If you use different ViewModels you should update the secornd user control view model in code when the first user control is updated.

Related

Event handler can't be found in MVVM

I have a silverlight application. One of StackPanel will display the table. The first column is a check box.
<telerik:RadGridView.Columns>
<telerik:GridViewColumn Width="80" Header="Complete" HeaderTextAlignment="Center" TextAlignment="Center">
<telerik:GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox HorizontalAlignment="Center" IsChecked="{Binding Something, Converter={StaticResource ShortToBooleanConverter}}" Checked="Complete_Checked"></CheckBox>
</DataTemplate>
</telerik:GridViewColumn.CellTemplate>
</telerik:GridViewColumn>
What I want is that once I click the box, a message box with Y/N pops up. I do have a Complete_Checked method in MVVM. But I get the error
Event handler 'Complete_Checked` not found on class.....
You can't use click event handlers with MVVM you need to use CommandBinding or DataBinding depending on what you're doing.
With your example you'll use data binding. You want to bind to the checkbox dependency property called IsChecked. You'll also want to use the Mode of TwoWay. This will allow the UI to update the bound property when it changes.
<CheckBox IsChecked="{Binding CheckBoxIsChecked, Mode=TwoWay}">
Then in your object model not viewmodel
private bool _checkBoxIsChecked;
public bool CheckBoxIsChecked
{
get{ return _checkBoxIsChecked;}
set{_checkBoxIsChecked = value; OnPropertyChanged("CheckBoxIsChecked"); }
}

WPF ListBox binding works at first, but not afterwards

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.

How do you bind a TextBox to a ListBox's selected member's corresponding property?

I have a list of notifications:
public List<Notification> Notifications { get; set; }
A ListBox in my UI binds to this list:
<ListBox ItemsSource="{Binding Notifications}" DisplayMemberPath="ServiceAddress" Name="NotificationsList"/>
In my UI, I also have this TextBox:
<TextBox Name="MatchWindowTextBox"/>
MatchWindow is a property in Notification objects...so I can access it like this from the above list: Notifications[SomeIndex].MatchWindow. Anyways, when someone changes the selection on the ListBox, this effectively selects a different Notification...so is there some way to bind my TextBox to the selected notification's MatchWindow property?
one quick way is to use an ElementName binding:
<TextBox Text="{Binding SelectedItem.MatchWindow,
ElementName=NotificationsList}"/>
However, a better approach would be to create a SelectedItem property of type Notification in the ViewModel and bind to that instead:
<ListBox ItemsSource="{Binding Notifications}"
SelectedItem="{Binding SelectedNotification}"/>
<!-- ... -->
<TextBox Text="{Binding SelectedNotification.MatchWindow}"/>

How to bind a button on wpf grid to a method on MVVM when I am using caliburn micro

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

MVC design issue

I'm having an application using MVC. It has a canvas and property grid. When an item is selected in the canvas. The property grid should display its details.
So I made an event listener and when item is selected in the canvas it raises an event to the controller which pass the selected item to the property grid to display the details.
Model :
Item object containing name, description
Controller :
protected Controller(object model, FrameworkElement view)
{
this._model = model;
this._view = view;
}
public virtual void Initialize()
{
View.DataContext = Model;
}
View :
<TextBlock>Status</TextBlock>
<ComboBox ItemsSource="?????"/>
Where view is the property grid and model is the selected item.
The problem is in the property grid there is a dropdown list containing lookup values how can I get the dropdown values given that the datacontext of the property grid has already been set to the selected item which doesn't contain reference to these lookup items.
I know that it's easy to use custom code to do that. But I don't want to violate the MVC aproach.
Bind to a source rather than DataContext, sources are provided by ElementName, RelativeSource & Source, so you can name the View for example and use ElementName to get it as source then the Path could be DataContext.LookupValues or whatever your property in the model (- the DataContext of the View is your model -) is called.
e.g.
<Window ...
Name="Window">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}" />
<ComboBox ItemsSource="{Binding ElementName=Window, Path=DataContext.Occupations}"
SelectedItem="{Binding Occupation}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- ... --->
Edit: Your problem seems to be that you do not pass the information you need, consider a design which still grants you access to more than just the SelectedItem of some list, e.g.
<Window ...
Name="Window">
<ListBox Name="listBox" ItemsSource="{Binding Data}" />
<ContentControl DataContext="{Binding ElementName=listBox, Path=SelectedItem}">
<ComboBox ItemsSource="{Binding ElementName=Window, Path=DataContext.Occupations}"
SelectedItem="{Binding Occupation}" />
</ContentControl>
<!-- ... --->
The DataContext of the ContentControl may be the SelectedItem of the ListBox but the ComboBox inside can still reference the DataContext of the Window which should provide the necessary information.
This is similar to my first example in that the DataContext inside the DataTemplate is always an item of the collection but you can access external DataContexts using sources in your bindings.

Categories

Resources