I have a CollectionView, on its data template has a progressbar. I'm able to find the respective element index of ObservableCollection but how can I reference its respective ProgressBar view? I need call method ProgressTo(), or may I simply bind the progress property to a property of the item on collection?
I'm afraid hat you can not use ProgressTo directly, because you can not access Progreeebar control in CollectionView directly.
If you still want to get ProgressBar, and call ProgressTo() method, you can consider to add Button in CollectionView datatemplate, like this:
<CollectionView ItemsSource="{Binding barmodels}" SelectionMode="Single">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding str}" />
<ProgressBar Progress="{Binding value}" />
<Button Clicked="Button_Clicked" Text="btn1" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
Then you can get current ProgressBar control by Button.Click.
public partial class Page7 : ContentPage
{
public ObservableCollection<barmodel> barmodels { get; set; }
public Page7()
{
InitializeComponent();
barmodels = new ObservableCollection<barmodel>()
{
new barmodel(){str="test 1",value=0.1},
new barmodel(){str="test 2",value=0.2},
new barmodel(){str="test 3",value=0.3},
new barmodel(){str="test 4",value=0.4},
new barmodel(){str="test 5",value=0.5}
};
this.BindingContext = this;
}
private void Button_Clicked(object sender, EventArgs e)
{
// access buttonclickhandler
var buttonClickHandler = (Button)sender;
// access Parent Layout for Button
StackLayout ParentStackLayout = (StackLayout)buttonClickHandler.Parent;
ProgressBar progressbar = (ProgressBar)ParentStackLayout.Children[1];
progressbar.ProgressTo(0.75, 500, Easing.Linear);
}
}
public class barmodel
{
public string str { get; set; }
public double value { get; set; }
}
But I don't suggest you to do it, I think use binding Progress for ProgressBar is the best way.
Related
I am having a ListView in which items are added incrementally using ItemAppearing. I want it to implement it through my ViewModel. ItemAppearing only calls a method from View.cs hence, is there any way I could implement it in my ViewModel class.
Please note that I can load incrementally items when adding it from View.cs. I just want to load more items from ViewModel.
Here is my XAML code:
<ListView ItemsSource="{Binding JobsList}" HasUnevenRows="True"
SelectedItem="{Binding SelectedJob}" ItemAppearing="LoadMoreItems">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Label Text="{Binding Title}" />
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
It would be best if you add (to your question) more details about what you are trying to do.
The following technique blurs the line between "what View does" and "what ViewModel does" - and "tightly couples" your View and your ViewModel - it is possible that there is a better way to approach your goal. Nevertheless, this is a useful technique to know, so I'll show it.
Details for Jason's comment "call your VM method from event handler".
Add a public method to your VM:
public class MyVM
{
public void MyMethod() {
// whatever you need to do to prepare the item.
}
}
In LoadMoreItems, call that method:
((MyVM)BindingContext).MyMethod();
Add parameters as needed.
If you need to "call back" to a method in your View, do that via an action parameter:
public void MyMethod(Action<...> action) {
...
action(...);
}
For more details, google C# passing an Action as a parameter.
There are other techniques for communicating between View and ViewModel - search for more info on that topic.
This answer will be moderately lengthy, but you will not find it anywhere else. I have tried for 2 days.
This answer is lengthy because I have demonstrated 3 things:
Binding ItemAppearing to Command and then incrementally loading items.
Selecting an Item from ListView and displaying it.
Showing animation while loading new items incrementally.
In MVVM, ViewModel is supposed to be ignorant of the View. Hence ViewModel must not know if the ListView inside the View is scrolled to the last item or whether LoadMoreItems should be called on scrolling.
We need to convert ItemAppearing Event to Command and Bind this Command to the ItemAppearing event.
For this purpose we need to install Xamarin.CommunityToolkit Nuget Package. This package is supported by .NetFoundation, Xamarin Community and Microsoft, and is authored by Microsoft. This is the official package and is necessary for most of the advanced Xamarin.Forms. Check more on Nuget.org, Download latest stable release: https://www.nuget.org/packages/Xamarin.CommunityToolkit (Install in all your projects Shared, Android, iOS, UWP, WPF, Tizen, etc)
Assume your Model:
public class Job
{
public int Id { get; set; }
public string Title { get; set; }
}
Now in your XAML
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
...
xmlns:xct="http://xamarin.com/schemas/2020/toolkit">
<StackLayout>
<RefreshView IsRefreshing="{Binding IsLoading}">
<ListView ItemsSource="{Binding JobsList}" SelectedItem="{Binding SelectedJob}">
<ListView.Behaviors>
<xct:EventToCommandBehavior EventName="ItemAppearing"
Command="{Binding LoadMoreItemsCommand}"
CommandParameter="{Binding ItemVisibilityEventArgs}"/>
</ListView.Behaviors>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Id}" />
<Label Text="{Binding Title}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
In your code behind set BindingContext to instance of the ViewModel
public partial class ListPage : ContentPage
{
ListPageViewModel ListPageVM;
public JobsListPage()
{
InitializeComponent();
ListPageVM = new ListPageViewModel();
BindingContext = ListPageVM;
}
}
In Your ViewModel
public class ListPageViewModel : INotifyPropertyChanged
{
public ObservableCollection<Models.Job> JobsList
{
get { return jobsList; }
set { jobsList = value; OnPropertyChanged(nameof(JobsList)); }
}
public ICommand LoadMoreItemsCommand { get; private set; }
// Used to show Loading Animation in Refresh View
public bool IsLoading
{
get { return isLoading; }
set { isLoading = value; OnPropertyChanged(nameof(IsLoading)); }
}
public Models.Job SelectedJob
{
get { return selectedJob; }
set
{
if (value != null)
{
selectedJob = value;
var page = Application.Current.MainPage;
page.DisplayAlert("Alert", $"Selected: {selectedJob.JobTitle}", "OK");
OnPropertyChanged(nameof(SelectedJob));
}
}
}
ObservableCollection<Models.Job> jobsList;
Models.Job selectedJob;
bool isLoading;
public ListPageViewModel() // ViewModel Constructor
{
// Initialize your List
JobsList = new ObservableCollection<Models.Job>
{
new Models.Job() { Id = 0001, Title = "Product Manager" },
new Models.Job() { Id = 0002, Title = "Senior Executive" },
}
LoadMoreItemsCommand = new Command<ItemVisibilityEventArgs>(
execute: async (ItemVisibilityEventArgs args) =>
{
if ((args.Item as Models.Job).Id >= JobsList[JobsList.Count - 1].Id)
{
IsLoading = true;
for (int i = 0; i < 10; i++)
{
JobsList.Add(new Models.Job()
{
Id = JobsList.Count + 1, JobTitle = JobsList[i].Title
});
}
await System.Threading.Tasks.Task.Delay(2000); // Fake delay
IsLoading = false;
}
});
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I am binding my ListView to my Realm database, but it doesn't refresh it when I add a new item into it. I am adding new items using another page.
I am using here as reference: https://help.syncfusion.com/xamarin/sflistview/mvvm#binding-itemssource
My Model:
public class Category : RealmObject
{
[PrimaryKey]
public string CategoryID { get; set; } = Guid.NewGuid().ToString();
public string CategoryTitle { get; set; }
public string CategoryDetails { get; set; }
[Backlink(nameof(Note.CategoryOfNote))]
public IQueryable<Note> Notes { get; }
public string CategoryIcon { get; set; }
public bool IsExpanded { get; set; }
}
My XAML file containing ListView
<ContentPage.BindingContext>
<vm:MainViewModel />
</ContentPage.BindingContext>
<ContentPage.ToolbarItems>
<ToolbarItem Text="NEWCAT"
Clicked="NewCat_Clicked"/>
</ContentPage.ToolbarItems>
<ContentPage.Content>
<ListView x:Name="categoryList"
ItemsSource="{Binding Categories}"
ItemTapped="ListView_ItemTapped"
HasUnevenRows="True">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Vertical"
HorizontalOptions="FillAndExpand"
Padding="10"
Spacing="10">
<Label Text="{Binding Path=CategoryTitle}"
FontSize="Medium"/>
<StackLayout IsVisible="{Binding IsExpanded}"
Orientation="Horizontal"
HorizontalOptions="CenterAndExpand">
<Button Text="Notes"
Clicked="NotesButton_Clicked" />
<Button Text="Edit"
Clicked="EditButton_Clicked"/>
<Button Text="Delete"
Clicked="DeleteButton_Clicked"/>
</StackLayout>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage.Content>
And my ViewModel
(DBServices.GetCategories is a static method that returns a collection of categories from Realm DB. And BaseViewModel implements INotifyPropertyChanged)
class MainViewModel : BaseViewModel
{
private Category _oldCategory;
public MainViewModel()
{
RefreshCategories();
}
private void RefreshCategories()
{
Categories = new ObservableCollection<Category>(DBServices.GetCategories());
}
private ObservableCollection<Category> _Categories;
public ObservableCollection<Category> Categories
{
get
{
return _Categories;
}
set
{
_Categories = value;
OnPropertyChanged("Categories");
}
}
}
This is OnPropertyChanged method in BaseViewModel class
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
When I click to the toolbar button, it pushes a new form page to create a new category. Then it just adds that category to the Realm DB. Afterwards it pops itself. But I see no change in my ListView.
EDIT: I can see new items in ListView when I restarted the app. But I want it to be listed as I add them.
You could use the Xamarin Forms MessagingCenter to tell the ListView it needs to update.
On the page where the new category is added to the Realm DB, at some point after the category is added but before the page is popped, you would send a message to tell the ListView page to update. This may look something like:
MessagingCenter.Send(this, "Update listview");
In your MainViewModel you'll need to subscribe to the message and then act upon it. This could be something like:
MessagingCenter.Subscribe<AddCategoryPage>(this, "Update listview", (sender) =>
{
RefreshCategories();
});
Two items to note when using the MessagingCenter.
The message passed must be identical between the sender and subscriber. In this case the message is "Update listview". Because it must be identical, you would probably want to define the message in separate class so it can be referenced by both the sender and subscriber.
When subscribing to a message you have to make sure that you unsubscribe. A popular pattern is to subscribe when the page appears and unsubscribe when the page disappears. A different pattern may work better for you though.
On a side note, you shouldn't need the full property definition for Categories. The ObservableCollection will handle the notification when items are added or removed. Just set Categories = new ObservableCollection<Category>(); once and then only add or remove items. You don't need to new it up on every DB call.
To test the XAML and ViewModel only, try replacing DBServices.GetCategories() with the following in the ViewModel:
public static IEnumerable<Category> TestCategories
{
get
{
yield return new Category { CategoryTitle = "title 1" };
yield return new Category { CategoryTitle = "title 2" };
}
}
...
public MainViewModel()
{
Categories = new ObservableCollection<Category>(TestCategories);
}
DBServices.GetCategories() should also be tested for expected output, not shown here.
I'm trying to do some basic UI and binding in UWP to get my head wrapped around it but I'm having trouble accessing a button within a listview item.
I have a Button where on clicking it, it creates a new object which is added to my ObservableCollection which then ends up adding a new item in my ListView
XMAL
<ListView Grid.Row="1" ItemsSource="{x:Bind counts}" x:Name="buttonsView" Margin="5,0" Background="White" Foreground="#FF5059AB">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Counter">
<StackPanel Orientation="Vertical">
<TextBlock Text="{x:Bind CountValue, Mode=TwoWay}" FontWeight="Black"/>
<Button Click="Increment_Click" Content="Increment"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
C#
public class Counter
{
private int count;
public Counter()
{
count = 0;
}
public int CountValue
{
get
{
return count;
}
set
{
count = value;
}
}
}
public sealed partial class MainPage : Page
{
ObservableCollection<Counter> counts = new ObservableCollection<Counter>();
public MainPage()
{
this.InitializeComponent();
}
private void Increment_Click(object sender, RoutedEventArgs e)
{
// ?
}
private void AddCounter_Click(object sender, RoutedEventArgs e)
{
counts.Add(new Counter());
}
}
That works.
But when I click on the button within the StackPanel, the Increment_Click method is called but I'm not sure what to do at that point. I would like to access the Counter object by getting the index of the ListView item and using that to index into the ObservableCollection.
How do I figure out what the index is of the ListView item that was added?
Instead of an event you should use Command and CommandParameter. You would then bind the Command to a command implemented in Counter and CommandParameter to the item (like {Binding}).
However, you can achieve your goal with Click event as well using DataContext:
private void Increment_Click(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
var counter = (Counter)button.DataContext;
//...
}
Basically DataContext is by default inherited from the parent unless you specify otherwise, so in this case DataContext is the current list item.
I have an listbox, with data inside(nutrition plans) and an mousedoubleclick function that opens an data about certain nutrition plan information(daily nutrition plans) in new page. The GetDailyNutrition class gets the Id from the first listbox.
If I click on "nutrition plan A" on the listbox, it should display the daily nutritions in a different listbox(on the nutritionPlan.xaml view).
The problem is, I could not get to show the daily nutritions in the listbox.
my NutritionPlan.xaml.cs code:
public partial class NutritionPlan : Page
{
private DailyNutritionPlanVM _dailyNutritionplanvm;
public Models.NutritionPlan _NPlan;
public NutritionPlan(Object NPlan)
{
_NPlan = NPlan as Models.NutritionPlan;
InitializeComponent();
}
public void getDailyNutrition()
{
_dailyNutritionplanvm = new DailyNutritionPlanVM();
_dailyNutritionplanvm.LoadData(_NPlan.NutritionPlanId);
DataContext = _dailyNutritionplanvm;
}
}
My NutritionPlan.Xaml code:
<Grid Background="LightBlue">
<ListBox HorizontalAlignment="Left" Height="123" Margin="88,220,0,0" VerticalAlignment="Top" Width="162">
</ListBox>
</Grid>
Listbox where the data is taken:
<ListBox x:Name="NutritionPlansControlListBx" Margin="10,5" Height="282" ItemsSource="{Binding NutritionPlans}"
MouseDoubleClick="ListBox_MouseDoubleClick">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding NutritionPlanName}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
MouseDoubleClick class:
private void ListBox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
_frame.Content = new NutritionPlan(NutritionPlansControlListBx.SelectedItem);
}
First, your view should not know about Model. so I moved the model to view model.
Also you have to be careful with data context since each view only got one. so I changed its type to a container view model type which contains daily plans. At this point your NutritionPlan view starts with one object of NutritionPlanVM.
NutritionPlan.xaml.cs:
public partial class NutritionPlan : Page
{
public NutritionPlanVM ViewModel { get { return DataContext as NutritionPlanVM; } set { DataContext = value; } }
public NutritionPlan(Object NPlan)
{
InitializeComponent();
//ViewModel = new NutritionPlanVM(NPlan);
//or can be ViewModel = a VM selected from RootVM
}
}
This view model needs its LoadData method to be explicitly called in order to create multiple daily plans for itself.
NutritionPlanVM.cs:
public class NutritionPlanVM : DependencyObject
{
Models.NutritionPlan _NPlan;
private ObservableCollection<DailyNutritionPlanVM> _dailyPlans;
public ObservableCollection<DailyNutritionPlanVM> DailyPlans { get { return _dailyPlans; } }
public void LoadData(Models.NutritionPlan _NPlan)
{
var dnpVM = new DailyNutritionPlanVM(_NPlan.NutritionPlanId);
_dailyPlans.Add(dnpVM);
}
}
You need another view model containing an observable collection for all nutrition plans.
public class RootVM : DependencyObject
{
private ObservableCollection<NutritionPlanVM> _nutritionPlans;
public ObservableCollection<NutritionPlanVM> NutritionPlans { get { return _nutritionPlans; } }
//vm data populated in constructor
public RootVM()
{
_nutritionPlans.Add([add all nutrition plans]);
}
}
Now for the binding root, there should be a view corresponding to this RootVM. (e.g., MainWindow). in the constructor of that view you need DataContext = new RootVM()
Now that everything is in place, you can bind to the currently selected item of the first listbox, and use that item to extract view model information:
<ListBox ItemsSource="{Binding ElementName=NutritionPlansControlListBx, Path=SelectedItem.DailyPlans}"/>
I have several UI elements in a DataTemplate bound to an ObservableCollection of Video objects. I want to call a method of a Video object when I click on the ContextMenuItem [Test] of the corresponding UI element.
Here is my XAML:
<ItemsControl Name="VideoUIElment" >
<ItemsControl.ItemTemplate>
<DataTemplate x:Uid="videoTemplate">
<Border CornerRadius="10" Padding="10, 10" Background="Silver" >
<TextBlock Name="label" Text="{Binding Name}" FontSize="30" Foreground="Black" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock.ContextMenu>
<ContextMenu>
<MenuItem Header="[TEST]" Name="Test" Click="Test_Click"/>
</ContextMenu>
</TextBlock.ContextMenu>
</TextBlock>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Here is the collection:
public MainWindow()
{
//ctor
InitializeComponent();
pathToLauncher = string.Empty;
videos = new ObservableCollection<Video>();
VideoUIElment.ItemsSource = videos;
}
I know that, to do this, I have to identify which Video object inside the collection is bound to the specific UI element I click on, and I could come up with some trick to achieve this, but I would like to do it in a graceful and intelligent way.
I've seen some suggestions already, but none of them seemed to have been applicable here. I guess, it is supposed to be something easy, but I'm not very versed in WPF yet.
Try this:
MainWindow:
public partial class MainWindow : Window
{
ObservableCollection<Video> videos { get; set; }
public MainWindow()
{
InitializeComponent();
videos = new ObservableCollection<Video>
{
new Video {Name = "Video 1"},
new Video {Name = "Video 2"},
new Video {Name = "Video 3"}
};
VideoUIElment.ItemsSource = videos;
}
private void Test_Click(object sender, RoutedEventArgs e)
{
MenuItem item = (MenuItem)sender;
Video video = (Video)item.DataContext;
MessageBox.Show(video.VideoMethod());
}
}
Video:
public class Video
{
public string Name { get; set; }
public string VideoMethod()
{
return string.Format(" Clicked {0}", Name);
}
}