WPF: binding through custom dependency property - c#

I have an TabControl bound to a Dictionary and has a custom control as it's ContentTemplate. The custom control has a custom dependency property Schedules and it's DataContext is bound to a ViewModel, Here is how it look like:
Main control:
<TabControl Grid.Row="1" ItemsSource="{Binding Schedules}">
<TabControl.ContentTemplate>
<DataTemplate>
<TabControl >
<TabItem Header="Scheduled flights">
<views:MyViewer Schedules="{Binding Value}"/>
</TabItem>
</TabControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
In MyViewer, I also have an DataGrid that I want it to be bound the Schedules passed from the TabControl, but in the same time MyViewer has a ViewModel assigned to it. This is how it looks like in MyViewer:
<DataGrid Grid.Row="1" ItemsSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}, Path=Schedules}" AutoGenerateColumns="False" >
So far this is not working, when MyViewer is loaded Schedules property is null. But even if it works, I would want the Schedules to be passed to the ViewModel not code behind. One idea is to populate the Dictionary with ViewModels of MyViewer, but I do not wish to do this, I only want the Main control to know about details of MyViewer. So any clean idea to solve this?
EDIT:
The proposition above does work after changing to ObservableDictionary, but the question remains, how to have the Schedules in the ViewModel

If MyViewer has it's own ViewModel, you should not rather do hacks like this:
ItemsSource="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=UserControl}, Path=Schedules}"
This way MyViewer is tightly coupled to some other control.
Im my opinion, in this case, MyViewer's ViewModel should have it's own property Schedules. How could you keep TabControl ViewModel's Schedules and MyViewer ViewModel's Schedules in sync? It depends on your system, but you could try with this ideas:
Sending ViewModel level messages like in MVVM Light, when adding or removing items. Example in this blog post
Try to implement some kind of store like in NgXs or NgRx in Angular
Maybe you don't need to keep Schedules in sync - depends on your system? :)

Related

Prevent UserControl from rendering each time it is used

I am developing plain WPF application on top of .NET 5.0.
I have navigation mechanism implemented based on DataTemplate(s)
<DataTemplate DataType="{x:Type viewmodels:FirstViewModel}">
<views:FirstView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:SecondViewModel}">
<views:SecondView />
</DataTemplate>
and CurrentViewModel
<ContentControl Grid.Row="1" Content="{Binding CurrentViewModel}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
You can imagine there are navbar buttons changing the CurrentViewModel property.
Besides that I use Dependency Injection which provides all required services (including viewmodels).
Both FirstViewModel and SecondViewModel are singletons.
FirstView and SecondView are UserControl(s).
The problem:
Whenever I navigate between UserControls they re-render. This is very inconvinient, from performance perspective and application usability.
Let's say both UserControls have grids with searching features and more. I would like user to be able to navigate back to the same state which he left on the control (including focused elements and so on). Having singleton viewmodel allows to preserve state/values of the properties but it doesn't solve the problem of control being re-rendered whenever I navigate. I clearly see that each time FirstView constructor is being called on which it InitializeComponent() and later calls OnRender().
Grids have asynchronous Sources.
<DataGrid Style="{DynamicResource DataGridStyle}" ItemsSource="{Binding ActiveSearchResults, IsAsync=True}" AutoGenerateColumns="False" CanUserSortColumns="True" CanUserAddRows="False">
.
.
.
</DataGrid>
I don't want grid to be re-rendered each time I navigate and elements to lose their focus. When I navigate I also see the grid flikering.
Can you please suggest option I have?
Is it possible to have singleton UserControl?
Is it possible to have static property with a cached state?

Find Listbox in DataTemplate inside ItemsControl

To create my own drophandler I need to get access to the listbox which is inside an ItemsControl.
XAML
<ItemsControl ItemsSource="{Binding Days}" Name="myCalendar" Margin="200,75,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="6" Columns="7">
</UniformGrid>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- ItemTemplate -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Date}">
</TextBlock>
<ListBox Name="Scenes" ItemsSource="{Binding Scenes}" dd:DragDrop.IsDragSource="True" dd:DragDrop.IsDropTarget="True" dd:DragDrop.DropHandler="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock>
<Run Text="{Binding Path=SlugLine}"/>
</TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
How do I get access or find the ListBox inside the ItemsControl from my ViewModel, not via code behind?
You absolutely do not want the VM knowing about the view. The whole point of MVVM is to decouple the view and the logic.
Instead handle the drop in the code-behind. Some people seem to believe that there should be no code-behind in MVVM, but it's absolutely fine as long as it's specific to the view, and there is no VM logic in there.
Imagine that you have hooked up a completely new view to your VM, say a console based text view. If your view logic remains intact with a completely new view, since it's all in the VM then you're fine. If you have logic in the code-behind that would disappear when you changed views, then you need that logic moved down to the VM.
Drag and drop is fine. You handle the drop in code-behind and then call the VM to do the logic associated with the drop, say via a bound command. If replacing the view with a text view, the drop could be CTRL-V instead, but the same VM command would be called to do the logic associated with the drop.
As mentioned, one way to call the VM from the code-behind would be to have a dependency property on the view that gets bound to a command in the VM, with your code-behind just invoking the command via the property.
A simpler way is to just cast the DataContext to your VM type and call a function directly. A lot of people dislike this since it couples the view to a VM type, but I see no issue with it at all. The view is already coupled to all bound properties on the VM anyway. VM's should be view agnostic, but the view NEEDS to know about the VM in order to be useful.

WPF MVVM Navigation Technique

I know there are a lot of questions about WPF navigation, for application developed with MVVM pattern, and I have read tens and tens of answers but I'm missing probably something.
I started building an application following Rachel's article here. All works just fine, there's an ApplicationView Window with this XAML:
<Window x:Class="CashFlow.ApplicationView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:CashFlow.ViewModels"
xmlns:v="clr-namespace:CashFlow.Views"
Title="ApplicationView" Height="350" Width="600" WindowStartupLocation="CenterScreen">
<Window.Resources>
<!--Here the associations between ViewModels and Views-->
<DataTemplate DataType="{x:Type vm:HomeViewModel}">
<v:HomeView />
</DataTemplate>
</Window.Resources>
<!--Define here the application UI structure-->
<DockPanel>
<Border DockPanel.Dock="Left" BorderBrush="Black" BorderThickness="0,0,1,0">
<ItemsControl ItemsSource="{Binding PageViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding Name}"
Command="{Binding DataContext.ChangePageCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}"
CommandParameter="{Binding}"
Margin="2,5" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<ContentControl Content="{Binding CurrentPageViewModel}" />
</DockPanel>
The ApplicationViewModel, that is set as DataContext for this window when the application starts, maintains an ObservableCollection of my ViewModels. Thanks to data templates, it's possible to associate every view with its viewmodel, using a ContentControl to render the views. Navigation in this case is accomplished with a "side bar" of buttons, binded to ApplicationViewModel commands that perform the changes of CurrentPageViewModel object.
I'm wondering how I can perform navigation without the presence of that sidebar of Buttons. Having only the Content control, I should be able to change the CurrentPageViewModel from the others viewmodel? Probably the answer will be very trivial, but I can't see that right now.
Your top level homeviewmodel can orchestrate navigation via an eventbus pattern. To use eventbus, you would inject an object that tracks objects that want to be notified of events. Then when a view model raises an event, the homeviewmodel receives it and performs the currentpageviewmodel assignment that will navigate you to the next viewmodel.
Ex:
Messenger defines two methods - RegisterForEvent<IEvent>(ViewModel aViewModel), and RaiseEvent(IEvent event).
So you would define a function to subscribe to the events -
HomeViewModel.cs
...
void SubscribeForEvents() {
Messenger.RegisterForEvent<NavigationEvent>(this);
}
Then you inject the Messenger into your other view models, and from those view models, raise the event:
Messenger.RaiseEvent(new NavigationEvent { TargetViewModel = new TargetViewModel() });
Where the event is something like
public class NavigationEvent : IEvent {
ViewModel TargetViewModel { get;set;}
}
C Bauer is right with what you are missing. I found in order to switch the data context, you'll need a messenger service to flag your "applicationviewmodel" to switch its data context. A good discussion with the steps you need are spelled out in a discussion here.
Register the message to be received in your applicationviewmodel, then handle the data context switch in your receive message function.
Also, this might be true or not, but I had to use 1 window, with multiple user controls as opposed to multiple windows if I wanted to have 1 window showing at all times. Lastly, I followed Sheridan's example and defined my data templates in my app.xaml as opposed to the window itself.

Call A Method From A Control That Is Not In The DataContext

I have a button in a UserControl that I want to call a method in another class (which happens to be my main window's view model).
<ToggleButton cal:Message.Attach="[Event Click] = [Action ToggleWatch]">
The user control's DataContext is tied to a DataTemplate. This makes everything beautiful, unless I run into cases like this where implementing the ToggleWatch method in the DataTemplate class doesn't make much sense, since the DataTemplate should only contain data.
Is the best way to get around this is by setting the DataContext of this control to the MainWindowViewModel? That workaround fails when I want to bind a value from the DataTemplate to the same button, since the DataContext modification will then make it look for the value in the MainWindowViewModel.
In case I've over complicated the question, I will summarise. I have a UserControl whose DataContext is a seperate DataTemplate. I want to call a method from a button when it is clicked, but I want the method seperated from the DataTemplate. I want the method in a completely different class. What is the best way to solve this?
Here is how I'm setting the DataTemplate:
<WrapPanel>
<ItemsControl ItemsSource="{Binding Devices}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:DeviceCleanerBox/>
...
UserControl must exist as child of your MainWindow and if i am right you must have set DataContext of MainWindow to be MainWindowViewModel.
So, what you can do is declare ICommand in MainWindowViewModel and bind to button's Command using RelativeSource to search for window's DataContext:
<ToggleButton Command="{Binding DataContext.CommandName,
RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType=Window}}"/>

WPF set DataTemplate on Viewmodel class programmatically

I have a listbox in WPF that will contain a list of ResultsViewModel items, however the actual runtime type of these objects could be
CalculateResultsViewModel,
ScenarioResultsViewModel,
GraphResultsviewModel etc etc,
all of which extend the base abstract class ResultsViewModel.
Each of these view models should be rendered differently in the ListBox so needs a different DataTemplate. I can do that just with XAML easy enough. The difficulty is that when the viewmodels are either "processing" or when they have failed", I need them to display a DataTemplate for "processing" or "errored" which I can only so far do with Triggers. That however then means I can't use the DataTemplateSelector or a basic XAML style.
The only solution I can think of (not clean I know) is to set the DataTemplate programmatically in the SetResult() method of each viewmodel class, which is what gets called when the processing completes either successfully or with an error. In that DependencyProperty I can look at the return code and then programatically set the DataTemplate depending on the sucess/failure result. The only problem is I cannot figure out how to
Obtain a DataTemplate resource from a ResourceDictionary just using c# code - bearing in mind Im calling all of this from the viewmodel class, not the window code-behind .xaml.cs file so it doesn't have access to the properties of Window
having only a handle to the viewmodel class, somehow obtain a reference to the ListBoxItem that contains it and then programmatically set the DataTemplate on this container.
Can anyone point me in the right direction?
you can take the magic with implicit datatemplates
<ListBox ItemSource={Binding YourResults}>
<ListBox.Resources>
<DataTemplate DataType={x:Type CalculateResultsViewModel}>
<Grid></Grid>
</DataTemplate>
<DataTemplate DataType={x:Type ScenarioResultsViewModel}>
<Grid></Grid>
</DataTemplate>
<DataTemplate DataType={x:Type GraphResultsviewModel }>
<Grid></Grid>
</DataTemplate>
</ListBox.Resources>
</ListBox>
for "processing" or "errored" viewmodels you can specify a adorner overlay in all yout datatemplates (ok but you must use the triggers)
hope this helps

Categories

Resources