I am writing a Universal app targeting Windows 8.1, and am re-writing it to use an IoC container. However, I found something that is puzzling me a bit.
Before I used the IoC, I would create an instance of my VM in the code-behind and bind to it, like this:
MainPage.xaml
<ListBox ItemsSource="{Binding ItemList}" DisplayMemberPath="Title" />
MainPage.xaml.cs
private MainPageVM Data = new MainPageVM();
public MainPage()
{
this.InitializeComponent();
this.DataContext = Data;
}
MainPageVM.cs
public ObservableCollection<MenuItem> ItemList { get; set; }
public MainPageVM()
{
ItemList = new ObservableCollection<MenuItem>();
}
This worked just fine. However, now I am setting up the app very differently, like this:
App.xaml.cs
private IUnityContainer _Container;
public App()
{
_Container = new UnityContainer();
this.InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
_Container.RegisterType<MainPage>(new ContainerControlledLifetimeManager());
_Container.RegisterType<Frame>(new ContainerControlledLifetimeManager());
_Container.RegisterType<MainPageVM>(
new ContainerControlledLifetimeManager(),
new InjectionConstructor(typeof(Frame),
typeof(MainPage)));
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame == null)
{
rootFrame = _Container.Resolve<Frame>();
rootFrame.CacheSize = 1;
}
if (rootFrame.Content == null)
{
var mainPageVM = _Container.Resolve<MainPageVM>();
mainPageVM.Show();
}
Window.Current.Activate();
}
This creates an instance of MainPageVM():
MainPageVM.cs
private Frame _Frame;
private Page _View;
public ObservableCollection<MenuItem> ItemList { get; set; }
public MainPageVM(
Frame frame,
Page view)
{
_Frame = frame;
_View = view;
_View.DataContext = this;
ItemList = new ObservableCollection<MenuItem>();
OnPropertyChanged("ItemList");
}
public void Show()
{
_Frame.Content = _View;
Window.Current.Content = _Frame;
}
MainPage.xaml.cs
public MainPage()
{
this.InitializeComponent();
}
MainPage.xaml is not changed.
My question is, why do I have to signal OnPropertyChanged("ItemList"); to activate the binding in the second version, but not the first? Am I doing something wrong here?
My entire repository can be found on GitHub: Learn OneNote.
Switching between these two lines should fix this:
_View.DataContext = this;
and
ItemList = new ObservableCollection<MenuItem>();
What happens is that you currently first set the DataContext, which causes all of the view's bindings to reevaluate, and only then you modify ItemsList. Since ItemsList's setter doesn't call OnPropertyChanged, the view is not updated when it's changed. By switching between the lines, you first initialize the data context and only then reevaluate bindings.
A more readable solution would be to include a call to OnPropertyChanged in your setter (then you don't have to switch the lines):
private ObservableCollection<MenuItem> itemList
public ObservableCollection<MenuItem> ItemList
{
get { return itemsList; }
set
{
if (itemsList != value)
{
itemsList = value;
OnPropertyChanged("ItemsList");
}
}
}
Although it's always a good practice to first initialize your data context and only then set it to avoid having controls to attempt binding twice.
That's because in the first version, ItemList instantiated in view-model constructor which itself constructed in view's initialization, before data binding. But in the second version, data get's bound first, then you instantiate the ItemList.
Related
I have been trying to solve the following problem for a very long time but unfortunately, I am unable to get it fixed.
I have a button which I want to disable it in another page .cd
This is how my code looks like:
<StackLayout>
<Button Text="Click" IsEnabled="{Binding IsButtonEnabled}" Command="{Binding OnEnabledButtonClicked}"/>
</StackLayout>
public class MainPageViewModel : BaseViewModel
{
bool _isButtonEnabled;
public bool IsButtonEnabled
{
get => _isButtonEnabled;
set
{
_isButtonEnabled = value;
OnPropertyChanged(nameof(IsButtonEnabled));
}
}
public Command OnEnabledButtonClicked
{
get
{
return new Command( () =>
{
IsButtonEnabled = true;
}
}
}
}
And this is the class where I want to change the value of VM's button.
public class Page1 {
class page1() {
InitializeComponent();
}
public void OnDisabledButtonClicked(object sender, EventArgs e) {
/// IsButtonEnabled = false;
}
}
I have already tried different ways but still no result.
It would be a big help for me if someone provides me a solution for it.
Thanks in advance
Before I gave you an answer, I would like to point out few things.
It's common to suffix your command with Command : EnabledButtonCommand.
I can see that your command is async while you don't await anything. It's bad.
Why would you want to set IsButtonEnabled in the code behind instead of in the method executed by the command (in the ViewModel) ?
Where do you set the DataContext ? Do you use Prism or anything else to associate the ViewModel to your page's DataContext ? If you don't, you need to do this :
public class MyPage()
{
private MyViewModel _viewModel = new MyViewModel();
public MyPage()
{
InitializeComponent();
DataContext = _viewModel;
}
public void OnDisabledButtonClicked(object sender, EventArgs e)
{
_viewModel.IsButtonEnabled = false;
}
}
If your ViewModel was set in Xaml or elsewhere (while navigating, with Prism, etc)
public class MyPage()
{
private MyViewModel _viewModel;
public MyPage()
{
InitializeComponent();
_viewModel = DataContext as MyViewModel;
}
public void OnDisabledButtonClicked(object sender, EventArgs e)
{
_myViewModel.IsButtonEnabled = false;
}
}
A last word : if a button is not enabled, the command/click event won't be available to user until the button is enabled again.
You can test that with a button with its command binded to the given command and another button with IsEnabled binded to your boolean.
The code I gave may have things wrong as I answer in browser without using an EDI.
I'm developing a Windows application (UWP) that has two pages, I want the best practice to pass parameters between pages.
it's my scenario:
We have two pages, each open and remain at the middle of the screen and a Button on each page, which send the message to the other page when we click on it.
I also want to pass information continuously and repeatedly.
in Page1.cs:
Page2 page2;
public Page1()
{
this.InitializeComponent();
CreatPage2();
}
// creat page 2
private async void CreatPage2()
{
var NewWindow = CoreApplication.CreateNewView();
int NewWindowid = 0;
await NewWindow.Dispatcher.RunAsync(CoreDispatcherPriority.High, () =>
{
Frame newframe = new Frame();
newframe.Navigate(typeof(Page2), this);
Window.Current.Content = newframe;
Window.Current.Activate();
ApplicationView.GetForCurrentView().Title = "page2";
NewWindowid = ApplicationView.GetForCurrentView().Id;
});
await Windows.UI.ViewManagement.ApplicationViewSwitcher.TryShowAsStandaloneAsync(NewWindowid);
}
//Button
private void ChangeP2_Click(object sender, RoutedEventArgs e)
{
// send a message to the texblock in the page2
page2.TexBlock2.Text=$"From page1 :{e.ToString()}";
// change text color of the texblock in the page2
page2.Foreground= new SolidColorBrush(Windows.UI.Colors.Red);
}
in Page2.cs:
Page1 page1;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
page1 = e.Parameter as Page1;
base.OnNavigatedTo(e);
}
public Page2()
{
this.InitializeComponent();
}
//Button
private void ChangeP1_Click(object sender, RoutedEventArgs e)
{
// send a message to the texblock in the page1
page1.TexBlock1.Text=$"From page2 :{e.ToString()}";
// change text color of the texblock in the page1
page1.Foreground= new SolidColorBrush(Windows.UI.Colors.Red);
}
the above code just work for the page2 to the page1. (it can change the textblock of pagea).
Please help me, I can't find a solution that work on two pages
Naah… the best way is to use a standard pattern that consist of an app ViewModel class, which contains all the common app data that you want to use in the logic layer.
I always do it like this:
1) I use the MainPage automatically created as the "shell" of the app, with a property that is the AppViewModel.
The MainPage (and thus the AppViewModel) can be accessed from everywhere in the app, by setting itself as a static field in its own class.
This is the code, simpler than you think:
public sealed partial class MainPage : Page
{
public AppViewModel ViewModel { get; set; } = new AppViewModel();
public static MainPage Current { get; set; }
public MainPage()
{
this.InitializeComponent();
Current = this;
}
}
2) The AppViewModel itself is a class that must implement the INotifyPropertyChanged interface, in order to enable bindable properties and functions.
It is common, among developers, to create a base class that implements it and then derive all the classes that needs bindable properties from it.
Here it is:
public class BaseBind : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetProperty<T>(ref T storage, T value,
[CallerMemberName] String propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
}
Then you derive AppViewModel class (and all the other model and viewmodel classes) from it… populating it with all the common properties that you need to share across pages.
I have even added a derived property, in order to show how you can share even multiple data types at once, and a function:
public class AppViewModel : BaseBind
{
public AppViewModel()
{
// ...
}
// All common app data
private string sampleCommonString;
public String SampleCommonString
{
get { return sampleCommonString; }
set { SetProperty(ref sampleCommonString, value); OnPropertyChanged(nameof(SampleDerivedProperty1)); OnPropertyChanged(nameof(SampleDerivedProperty2)); }
}
public String SampleDerivedProperty1 => "return something based on SampleCommonString";
public String SampleDerivedProperty2
{
get
{
<<evaluate SampleCommonString>>
return "Same thing as SampleDerivedProperty1, but more explicit";
}
}
// This is a property that you can use for functions and internal logic… but it CAN'T be binded
public String SampleNOTBindableProperty { get; set; }
public void SampleFunction()
{
// Insert code here.
// The function has to be with NO parameters, in order to work with simple {x:Bind} markup.
// If your function has to access some specific data, you can create a new bindable (or non) property, just as the ones above, and memorize the data there.
}
}
3) Then, in order to access all this from another Page, just create an AppViewModel field in that page, as seen below:
public sealed partial class SecondPage : Page
{
public AppViewModel ViewModel => MainPage.Current.ViewModel;
public SecondPage()
{
this.InitializeComponent();
}
}
...and you can easily bind XAML controls properties to the AppViewModel itself:
<TextBlock Text="{x:Bind ViewModel.SampleCommonString, Mode=OneWay}"/>
<Button Content="Sample content" Click="{x:Bind ViewModel.SampleFunction}"/>
(Mode=OneWay is for real-time binding, in order that the property is immediately updated even in the UI, while Mode=TwoWay is used for those properties that can be edited from the control itself, by the user, in order to interact with app logic).
Hope this helped.
Best regards and happy new year.
I have simplified app to show my issue
When I click button, it changes Text property of ViewModel and TextBlock.Text is updated.
MainPage.xaml
<StackPanel>
<Button Click="ButtonBase_OnClick">Button to change text</Button>
<TextBlock Text="{x:Bind ViewModel.Text, Mode=OneWay}"></TextBlock>
</StackPanel>
MainPage.xaml.cs
public MainPage()
{
ViewModel = new ViewModel();
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel.Text = "x:Bind works";
}
ViewModel class has one string property (Text) and implemented INotifyPropertyChange interface.
Problem starts when ViewModel is not set in ctor (i.e. viewModel is null and changed in runtime):
public MainPage()
{
//ViewModel = new ViewModel();//this line has been removed
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel();//this line has been added
ViewModel.Text = "x:Bind does not work";
}
Complited binding is not working (Text is not changed) and I could not figure out why it is so... I need to change viewModel from null (vm is null because it is waiting for some data in real app)
{x:Bind} bindings (often referred-to as compiled bindings) uses generated code to achieve its benefits. At XAML load time, {x:Bind} is converted into what you can think of as a binding object, and this object gets a value from a property on a data source. These generated code can be found in your obj folder, with names like (for C#) <view name>.g.cs.
For your code, the generated code will like following:
// Update methods for each path node used in binding steps.
private void Update_(global::UWP.BlankPage3 obj, int phase)
{
if (obj != null)
{
if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0)
{
this.Update_ViewModel(obj.ViewModel, phase);
}
}
}
private void Update_ViewModel(global::UWP.ViewModel obj, int phase)
{
this.bindingsTracking.UpdateChildListeners_ViewModel(obj);
if (obj != null)
{
if ((phase & (NOT_PHASED | DATA_CHANGED | (1 << 0))) != 0)
{
this.Update_ViewModel_Text(obj.Text, phase);
}
}
}
...
private global::UWP.ViewModel cache_ViewModel = null;
public void UpdateChildListeners_ViewModel(global::UWP.ViewModel obj)
{
if (obj != cache_ViewModel)
{
if (cache_ViewModel != null)
{
((global::System.ComponentModel.INotifyPropertyChanged)cache_ViewModel).PropertyChanged -= PropertyChanged_ViewModel;
cache_ViewModel = null;
}
if (obj != null)
{
cache_ViewModel = obj;
((global::System.ComponentModel.INotifyPropertyChanged)obj).PropertyChanged += PropertyChanged_ViewModel;
}
}
}
Here I just copy some method that related to your issue. From these method, you can find that before update TextBlock or PropertyChanged listeners, it will check if the ViewModel is null. If it is null, nothing will be done. So to make {x:Bind} work, we must initialize ViewModel before page loaded. And this is the reason why {x:Bind} doesn't work when you initialize ViewModel in Button.Click event.
To fix this issue, you can implement INotifyPropertyChanged interface for ViewModel like Filip said so that the generated code can be notified when ViewModel changed (from null to new ViewModel()) and update you UI.
But I think you can just initialize ViewModel in constructor. When you initialize ViewModel, you can set the properties that you are waiting for to null first like:
public MainPage()
{
ViewModel = new ViewModel() { Text = null };
this.InitializeComponent();
}
And then update these properties when your date is ready. In this way, you can do not implement INotifyPropertyChanged interface on your page.
Besides these, there is another cheaper way, you can call this.Bindings.Update(); method to force the bindings to be updated after you initialize ViewModel like following:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel();
ViewModel.Text = "x:Bind does not work";
this.Bindings.Update();
}
Did you implement INotifyPropertyChanged on page like so
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
private ViewModel viewModel;
public ViewModel ViewModel
{
get { return viewModel; }
set
{
viewModel = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ViewModel)));
}
}
public MainPage()
{
ViewModel = new ViewModel { };
this.InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
ViewModel = new ViewModel { };//this line has been added
ViewModel.Text = "x:Bind does not work";
}
public event PropertyChangedEventHandler PropertyChanged;
}
This works for me.
I developed an application on Windows 10 Universal App who use MVVM but I have a big problem with it.
I would add an ObservableCollection item(created on a second window) to the MVVM and then, show the new item on the ListView of MainPage but it doesn't refresh!
The 2 windows are always open
http://i.stack.imgur.com/WSo6v.jpg
The code of MVVMList.cs
public class MVVMList : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<THEFile> onglets_cache = new ObservableCollection<THEFile>();
public ObservableCollection<THEFile> onglets_list
{
get
{
return onglets_cache;
}
set
{
onglets_cache = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this,
new PropertyChangedEventArgs("onglets_list"));
}
}
public MVVMList()
{
onglets_list = new ObservableCollection<THEFile>();
Fonctions fonctions = new Fonctions();
fonctions.LoadOnglets(onglets_cache);
}
}
The code of the second page(always open) - CreateFile.xaml.cs
private void create_butt_Click(object sender, RoutedEventArgs e)
{
Fonctions fonc = new Fonctions(); MVVMList main = new MVVMList();
fonc.SetupNew(main.onglets_list, "test" + ".php", "");
}
//SetupNew on Fonctions.cs
public async void SetupNew(ObservableCollection<THEFile> list, string name, string content)
{
FolderPicker folderpick = new FolderPicker();
folderpick.ViewMode = PickerViewMode.List;
folderpick.FileTypeFilter.Add(".html"); folderpick.FileTypeFilter.Add(".htm"); folderpick.FileTypeFilter.Add(".HTML");
folderpick.FileTypeFilter.Add(".php"); folderpick.FileTypeFilter.Add(".PHP");
folderpick.FileTypeFilter.Add(".css"); folderpick.FileTypeFilter.Add(".CSS");
folderpick.FileTypeFilter.Add(".js"); folderpick.FileTypeFilter.Add(".JS");
StorageFolder storage_file = await folderpick.PickSingleFolderAsync();
if (storage_file != null)
{
MainPage vm = new MainPage();
list.Add(new THEFile { NameOfFile = name, PathOfFile = storage_file.Path + "\\" + name, CodeOfFile = content, already_opened = false, line = 0 });
string path = storage_file.Path + #"\" + name;
StorageFile file_create = await storage_file.CreateFileAsync(name, CreationCollisionOption.GenerateUniqueName);
Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.Add(file_create);
SaveOnglets(list);
}
}
And on the MainPage.xaml (always open)
...
<ListView x:Name="onglets" x:FieldModifier="public" ItemTemplate="{StaticResource Templa}" ItemsSource="{Binding onglets_list}" SelectionChanged="onglets_SelectionChanged" Margin="0,117,0,57" Visibility="Visible" ContainerContentChanging="onglets_ContainerContentChanging">
...
Thank you!
In your XAML, try using a Collection View Source.
Add this to the top of your xaml:
<Page.Resources>
<CollectionViewSource x:Name="MakesCollectionViewSource" IsSourceGrouped="True"/>
</Page.Resources>
Set your ListView:
ItemsSource="{Binding Source={StaticResource MakesCollectionViewSource}}"
Then in your code when you have a List of items assign it using
MakesCollectionViewSource.Source = /* Some List<GroupInfoList<object>> that is generated from onglets_list*/
I create my List like this but it may not be relevant because this is to make all of my object names alphabetical:
internal List<GroupInfoList<object>> GetGroupsByLetter()
{
var groups = new List<GroupInfoList<object>>();
var query = from item in MakeList
orderby ((Make)item).MakeName
group item by ((Make)item).MakeName[0] into g
select new { GroupName = g.Key, Items = g };
foreach (var g in query)
{
var info = new GroupInfoList<object>();
info.Key = g.GroupName;
foreach (var item in g.Items)
{
info.Add(item);
}
groups.Add(info);
}
return groups;
}
public class GroupInfoList<T> : List<object>
{
public object Key { get; set; }
public new IEnumerator<object> GetEnumerator()
{
return (System.Collections.Generic.IEnumerator<object>)base.GetEnumerator();
}
}
where MakeList is my observable collection and Make are the objects in the collection and MakeName is a string I am trying to alphabetize
And then call using
MakesCollectionViewSource.Source = GetGroupsByLetter();
If I understand your code and requirements correctly, I think part of the problem is that you "new up" your MVVMList and your MainPage everytime you click the create button.
So, without getting into suggestions about using MVVM Light and an IOC container, you could quickly accomplish what you're trying to do by making your MVVMList class a singleton and having your MainPage use it for a data context. When your other window adds to the MVVMList.onglets collection, it will be immediately reflected in your currently open MainPage. Let me know if you need some code snippets. Good luck!
[Edit below]
I had a few minutes left on lunch, so here is an over-simplified example. Again, without getting into what MVVM is and is not. Personally, I would do this differently, but that would be outside the scope of your question. Full disclosure - this is in WPF, but same logic applies, I just don't have Windows 10 on the PC that I'm using. I also simplified the collection to be of type string. This is not intended to copy/paste into your code as it will not work in your example - but should easily transfer.
MVVMList class:
public class MVVMList: INotifyPropertyChanged
{
//Singleton section
private static MVVMList instance;
private MVVMList() { }
public static MVVMList Instance
{
get
{
if (instance == null)
{
instance = new MVVMList();
}
return instance;
}
}
//end singleton section
private ObservableCollection<string> _onglets = new ObservableCollection<string>();
public ObservableCollection<string> Onglets
{
get { return _onglets; }
set
{
if (_onglets != value)
{
_onglets = value;
if (PropertyChanged != null)
PropertyChanged.Invoke(this,
new PropertyChangedEventArgs("onglets_list"));
}
}
}
//INotify implementation
public event PropertyChangedEventHandler PropertyChanged;
}
MainPage:
<ListView x:Name="onglets" x:FieldModifier="public" ItemsSource="{Binding Onglets}" />
MainPage.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = MVVMList.Instance;
Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var x = new CreateWindow();
x.Show();
}
}
CreateWindow.cs:
private void CreateButton_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrEmpty(StringTextBox.Text))
{
MVVMList.Instance.Onglets.Add(StringTextBox.Text);
}
}
I have observable collection called (Users) in view model that binded with ListViewControl (lstUsers) in view and what I need is to scroll to current logged in user in List View .
I see in most of examples that used scroll from code behind as following e.g. :
lstUsers.ScrollIntoView(lstUsers[5]);
but what I need is to handle it from view model .
Please advice !
One way of doing this would be to use something like an ICollectionView which has a current item. You can then set IsSynchronizedWithCurrentItem to true to link the current item in the view model to the selected item in the ListView.
Finally handle the event SelectionChanged in the code behind the view to change the scroll position so that it always displays the selected item.
For me the benefit of this method is that the viewmodel is kept unaware of anything about the view which is one of the aims of MVVM. The code behind the view is the perfect place for any code concerning the view only.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ListView x:Name="View"
SelectionChanged="Selector_OnSelectionChanged" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Items}"/>
<Button Grid.Row="1" Command="{Binding ChangeSelectionCommand}">Set</Button>
</Grid>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
private void Selector_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
View.ScrollIntoView(View.SelectedItem);
}
}
public class ViewModel
{
private readonly CollectionViewSource _source = new CollectionViewSource();
public ICollectionView Items
{
get { return _source.View; }
}
public ICommand ChangeSelectionCommand { get; set; }
public ViewModel()
{
SetUp();
ChangeSelectionCommand = new Command(ChangeSelection);
}
private void SetUp()
{
var list = new List<string>();
for (int i = 0; i < 100; i++)
{
list.Add(i.ToString(CultureInfo.InvariantCulture));
}
_source.Source = list;
}
private void ChangeSelection()
{
var random = new Random(DateTime.Now.Millisecond);
var n = random.Next(100);
Items.MoveCurrentToPosition(n);
}
}
public class Command : ICommand
{
private readonly Action _action;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
_action();
}
public event EventHandler CanExecuteChanged;
public Command(Action action)
{
_action = action;
}
}
let me share my solution with you
Create your own ListView descendant with dependency property TargetListItem
public class ScrollableListView : ListView
{
/// <summary>
/// Set this property to make ListView scroll to it
/// </summary>
public object TargetListItem
{
get { return (object)GetValue(TargetListItemProperty); }
set { SetValue(TargetListItemProperty, value); }
}
public static readonly DependencyProperty TargetListItemProperty = DependencyProperty.Register(
nameof(TargetListItem), typeof(object), typeof(ScrollableListView), new PropertyMetadata(null, TargetListItemPropertyChangedCallback));
static void TargetListItemPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var owner = (ScrollableListView)d;
owner.ScrollToItem(e.NewValue);
}
public void ScrollToItem(object value)
{
if (value != null && Items != null && Items.Contains(value))
{
ScrollIntoView(value);
}
}
}
create property in ViewModel
object currentListItem;
public object СurrentListItem
{
get => сurrentListItem;
set
{
if (сurrentListItem != value)
{
сurrentListItem = value;
OnPropertyChanged(nameof(СurrentListItem));
}
}
}
bind it
<controls:ScrollableListView ... TargetListItem="{Binding CurrentListItem}"/>
Now you can set CurrentListItem in ViewModel when needed. And the corresponding visual element will become visible in the ListView immediately.
Also maybe you just can use attached property on ListView instead of creating ScrollableListView. But i'm not sure.
Yep, there's always times in MVVM when you need to get at the control. There's various ways of doing this, but here's an easy-ish way of doing it without deriving from the control or messing with routed commands or other such toys what you have in WPF.
In summary:
Create an attached property on your view model.
Set the attached property in XAML to pass the list box back to the view model.
Call .ScrollIntoView on demand.
Note, this is a rough and ready example, make sure your DataContext is set before showing the window.
Code/View Model:
public class ViewModel
{
private ListBox _listBox;
private void ReceiveListBox(ListBox listBox)
{
_listBox = listBox;
}
public static readonly DependencyProperty ListBoxHookProperty = DependencyProperty.RegisterAttached(
"ListBoxHook", typeof (ListBox), typeof (ViewModel), new PropertyMetadata(default(ListBox), ListBoxHookPropertyChangedCallback));
private static void ListBoxHookPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var listBox = (ListBox) dependencyObject;
var viewModel = (ViewModel) listBox.DataContext;
viewModel.ReceiveListBox(listBox);
}
public static void SetListBoxHook(DependencyObject element, ListBox value)
{
element.SetValue(ListBoxHookProperty, value);
}
public static ListBox GetListBoxHook(DependencyObject element)
{
return (ListBox) element.GetValue(ListBoxHookProperty);
}
}
OK, so that will let us get the ListBox passed back to the view; you can do with it as you wish.
Now, just set the property in XAML:
<ListBox wpfApplication1:ViewModel.ListBoxHook="{Binding RelativeSource={RelativeSource Self}}" />
Good to go!