I'm trying to implement the XLabs CameraViewModel functionality into my Xamarin Forms App. Unfortunately, the given example uses XAML to bind the views with data, but i need to do it in code behind.
The following code is used to select a picture and get it's source.
public class CameraViewModel : XLabs.Forms.Mvvm.ViewModel
{
...
private ImageSource _imageSource;
private Command _selectPictureCommand;
public ImageSource ImageSource
{
get { return _imageSource; }
set { SetProperty(ref _imageSource, value); }
}
public Command SelectPictureCommand
{
get
{
return _selectPictureCommand ?? (_selectPictureCommand = new Command(
async () => await SelectPicture(),() => true));
}
}
...
}
And these commands are bound to XAML :
<Button Text="Select Image" Command="{Binding SelectPictureCommand}" />
<Image Source="{Binding ImageSource}" VerticalOptions="CenterAndExpand" />
How can I apply the same commands in code-behind for created elements?
CameraViewModel ViewModel = new CameraViewModel();
var Take_Button = new Button{ };
Take_Button.SetBindings(Button.CommandProperty, //*???*//);
var Source_Image = new Image { };
Source_Image.SetBinding(Image.SourceProperty, //*???*//);
I've successfully binded SelectPictureCommand by doing the following:
Take_Button .Command = ViewModel.SelectPictureCommand;
However I have my doubts about it being the correct way, and the same logic cannot be applies to ImageSource.
For the button you have:
var Take_Button = new Button{ };
Take_Button.SetBinding(Button.CommandProperty, new Binding { Path = nameof(ViewModel.SelectPictureCommand), Mode = BindingMode.TwoWay, Source = ViewModel});
For the image you have:
var Source_Image = new Image { };
Source_Image.SetBinding(Image.SourceProperty, new Binding { Path = nameof(ViewModel.ImageSource), Mode = BindingMode.TwoWay, Source = ViewModel });
Related
In reference to this question below I have XAML working, but am struggling with converting to C#
Xamarin / MAUI XAML to C#
Here is my attempt that isn't working and I'm not sure why... My goal is when LayoutState.Success it updated via VenuePageViewModel, VenueSuccessContentView should display inside the cvWrap ContentView on the Page.
public partial class VenuePageViewModel : ObservableObject
{
[ObservableProperty]
private LayoutState layoutState = LayoutState.Loading;
public VenuePageViewModel()
{
LayoutState = LayoutState.Success;
}
}
public class VenueSuccessContentView : ContentView
{
public VenueSuccessContentView()
{
Content = new Label() { Text = "hello world", TextColor = Colors.Red };
}
}
public class MainPageCS : ContentPage
{
public MainPageCS()
{
BindingContext = new VenuePageViewModel();
var venueSuccessCV = new VenueSuccessContentView();
Resources.Add(nameof(LayoutState.Success), venueSuccessCV);
var cvWrap = new ContentView();
var cv = new ContentView();
cvWrap.Content = cv;
Content = cvWrap;
var datatrigger = new DataTrigger(typeof(ContentView))
{
Binding = new Binding(source: RelativeBindingSource.TemplatedParent, path: nameof(VenuePageViewModel.LayoutState)),
Value = LayoutState.Success,
Setters = {
new Setter { Property = ContentView.ContentProperty, Value = venueSuccessCV },
}
};
cvWrap.Triggers.Add(datatrigger);
}
}
Minimal Repo: https://github.com/aherrick/DataTriggerCSharpMaui
The binding path is incorrect in DataTrigger .
Modify Binding property of DataTrigger as below .
Binding = new Binding( path: nameof(VenuePageViewModel.LayoutState))
I am trying to populate a collection view from a ViewModel, however when I try to bind the data to the collection view, the ViewModel is null.
xaml.cs file
ObservableCollection<ReportsClass> newKidList = new ObservableCollection<ReportsClass>();
public ReportsViewModel viewmodel { get; set; }
public ReportsPage()
{
InitializeComponent();
viewmodel = new ReportsViewModel();
this.BindingContext = viewmodel;
PreviousDateRange.CornerRadius = 20;
NextDateRange.CornerRadius = 20;
DateTime firstDate = currentDate.StartOfWeek(DayOfWeek.Sunday);
DateTime secondDate = currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday);
DateRange.Text = firstDate.ToString("MMMM d") + " - " + secondDate.ToString("MMMM d");
Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(viewmodel.kids));
}
Here is my view model
public class ReportsViewModel
{
public ObservableCollection<ReportsClass> kids { get; set; }
FirebaseStorageHelper firebaseStorageHelper = new FirebaseStorageHelper();
WebServiceClass webServiceClass = new WebServiceClass();
DateTime currentDate = DateTime.Now;
public ReportsViewModel()
{
GetKids();
}
public async void GetKids()
{
var parentId = await SecureStorage.GetAsync("parentid");
kids = await webServiceClass.Reports(Convert.ToInt32(parentId), currentDate.StartOfWeek(DayOfWeek.Sunday), currentDate.AddDays(7).StartOfWeek(DayOfWeek.Saturday));
}
}
And here is the method that gets the data for the view model
public async Task<ObservableCollection<ReportsClass>> Reports(int parentid, DateTime startDate, DateTime endDate)
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("parentid", parentid.ToString()),
new KeyValuePair<string, string>("startDate", startDate.ToString("yyyy-MM-dd H:mm:ss")),
new KeyValuePair<string, string>("endDate", endDate.ToString("yyyy-MM-dd"))
});
var response = await client.PostAsync(string.Format("https://example.com/api/index.php?action=reports"), content);
var responseString = await response.Content.ReadAsStringAsync();
ObservableCollection<ReportsClass> items = JsonConvert.DeserializeObject<ObservableCollection<ReportsClass>>(responseString);
return items;
}
What am I doing wrong? The purpose of me doing it this way is so I can update an item in the collectionview
Here is my ReportsClass
public class ReportsClass
{
public ReportsClass(string firstName)
{
first_name = firstName;
}
public string first_name { get; set; }
}
OPTION A:
Fix the syntax of Kids.SetBinding, to not get null. Refer to the CLASS ReportsViewModel, not to the INSTANCE viewmodel:
Kids.SetBinding(ItemsView.ItemsSourceProperty, nameof(ReportsViewModel.kids));
The kids still won't appear in list. To fix, kids needs OnPropertyChanged:
public ObservableCollection<ItemModel> kids {
get => _kids;
set {
_kids = value;
OnPropertyChanged();
}
}
private ObservableCollection<ItemModel> _kids;
See the other code in Option B. Adapt as desired.
When you need XAML to see a DYNAMIC change, you need OnPropertyChanged. This is an implementation of INotifyPropertyChanged. Add this call to properties (that XAML binds to) of ReportsClass:
// Inheriting from `BindableObject` is one way to obtain OnPropertyChanged method.
public class ReportsClass : Xamarin.Forms.BindableObject
{
public ReportsClass(string firstName)
{
first_name = firstName;
}
public string first_name {
get => _first_name;
set {
_first_name = value;
// This tells XAML there was a change.
// Makes "{Binding first_name}" work dynamically.
OnPropertyChanged();
}
}
private string _first_name;
}
OPTION B:
Didn't find an answer anywhere that does everything correctly, so here is a complete sample, for future reference:
Remove Kids.SetBinding(...). (It can be fixed as shown in OPTION A, but its easier to get it correct in XAML, so below I show it in XAML.)
Bindings from Page to VM. See xaml below.
Create ObservableCollection with setter that does OnPropertyChanged. This informs XAML when the list is ready, so page updates. (This is an implementation of INotifyPropertyChanged, as Jason mentioned.)
Use Device.BeginInvokeOnMainThread(async () to create an async context, that is queued to run after constructor returns. (This fixes the issue Jason mentioned, which is that a constructor isn't an async context, so should not DIRECTLY call an async method such as QueryItemsAsync, or your GetKids.) This is more reliable.
PageWithQueryData.xaml:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="TestXFUWP.PageWithQueryData">
<ContentPage.Content>
<StackLayout>
<CollectionView ItemsSource="{Binding Items}">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout>
<Label Text="{Binding Name}" />
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
<CollectionView.EmptyView>
<Grid>
<Label Text="Loading ..." FontSize="24" TextColor="Blue" BackgroundColor="LightBlue" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
</Grid>
</CollectionView.EmptyView>
</CollectionView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
PageWithQueryData.xaml.cs:
public partial class PageWithQueryData : ContentPage
{
public PageWithQueryData()
{
InitializeComponent();
// ... other initialization work here ...
// BUT remove `Kids.Binding(...);` line. See XAML: `ItemsSource="{Binding Items}"`.
BindingContext = new VMWithQueryData();
}
}
VMWithQueryData.cs:
class VMWithQueryData : Xamarin.Forms.BindableObject
{
public VMWithQueryData()
{
// Start an async task to query.
Xamarin.Forms.Device.BeginInvokeOnMainThread(async () => {
await QueryItemsAsync();
});
// Alternative implementation: Start a background task to query.
//QueryItemsInBackground();
}
public ObservableCollection<ItemModel> Items {
get => _items;
set {
_items = value;
OnPropertyChanged();
}
}
private ObservableCollection<ItemModel> _items;
private async Task QueryItemsAsync()
{
var names = new List<string> { "One", "Two", "Three" };
bool queryOneAtATime = false;// true;
if (queryOneAtATime) {
// Show each item as it is available.
Items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
// Simulate slow query - replace with query that returns one item.
await Task.Delay(1000);
Items.Add(new ItemModel(name));
}
} else {
// Load all the items, then show them.
// Simulate slow query - replace with query that returns all data.
await Task.Delay(3000);
var items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
items.Add(new ItemModel(name));
}
Items = items;
}
}
// Alternative implementation, using a background thread.
private void QueryItemsInBackground()
{
Task.Run(() => {
var names = new List<string> { "One", "Two", "Three" };
bool queryOneAtATime = false;// true;
if (queryOneAtATime) {
// Show each item as it is available.
Items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
// Simulate slow query - replace with query that returns one item.
System.Threading.Thread.Sleep(1000);
Items.Add(new ItemModel(name));
}
} else {
// Load all the items, then show them.
// Simulate slow query - replace with query that returns all data.
System.Threading.Thread.Sleep(3000);
var items = new ObservableCollection<ItemModel>();
foreach (var name in names) {
items.Add(new ItemModel(name));
}
Items = items;
}
});
}
}
ItemModel.cs:
public class ItemModel
{
public ItemModel(string name)
{
Name = name;
}
public string Name { get; set; }
}
This also demonstrates <CollectionView.EmptyView> to display a message to user, while the data is being queried.
For completeness, I've included an alternative QueryItemsInBackground, that uses a background thread instead of an async method. Either approach works well.
Notice inheritance from Xamarin.Forms.BindableObject. This is one way to get an implementation of INotifyPropertyChanged. You can use any other MVVM library or technique.
Move this line of code to the end of your constructor
this.BindingContext = viewmodel;
I have in my XAML the Image tag and in source a Binding SelectedImage. In the ViewModel I want to have the logic to add the image, but with my current code it does not insert the photo. Enter the gallery but when choosing the image do not show it.
This is my actual code:
MainPage.xaml:
<Image HeightRequest="50"
Source="{Binding SelectedImage}"
WidthRequest="50" />
ViewModel.cs:
string selectedimage;
public string SelectedImage
{
get => selectedimage; set
{
selectedimage = value;
OnPropertyChanged();
}
}
async void insertImage()
{
await CrossMedia.Current.Initialize();
if (!CrossMedia.Current.IsPickPhotoSupported)
{
return;
}
var mediaOptions = new PickMediaOptions()
{
PhotoSize = PhotoSize.Medium
};
var selectedImageFile = await CrossMedia.Current.PickPhotoAsync(mediaOptions);
if (SelectedImage == null)
{
return;
}
SelectedImage = Convert.ToString(ImageSource.FromStream(() => selectedImageFile.GetStream()));
}
just use the Path property of MediaFile
// this code does not do anything useful
SelectedImage = Convert.ToString(ImageSource.FromStream(() => selectedImageFile.GetStream()));
// do this instead
SelectedImage = selectedImageFile.Path;
Due to architecture design specifications, I have an application that fills its views from ClassLibraries. The application itself behaves like a sort of Integrator.
Now I need to add localization resources and I can successfully achieve it by adding *.resw files but only if the control is declared inside of the Application project.
What I actually need is to being able to share those resources across the ENTIRE SOLUTION somehow.
Then, the point is to being able to translate any control's content of the solution by using localization resources, preferably using the structure explained above.
For example, I have this following view, which fills the TextBlocks' content depending on the selected language:
<ComboBox x:Name="Languages"
ItemsSource="{Binding Languages}"
SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}">
<i:Interaction.Behaviors>
<iCore:EventTriggerBehavior EventName="SelectionChanged">
<iCore:InvokeCommandAction Command="{Binding ChangeLanguage}" />
</iCore:EventTriggerBehavior>
</i:Interaction.Behaviors>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding LanguageName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding Model.HelloText}" FontSize="50" Foreground="Red"/>
<TextBlock Text="{Binding Model.HowAreYouText}" FontSize="50" Foreground="Red"/>
<BFview:BFView />
</StackPanel>
Where BFView is a view stored in another project (has two dummy textblocks also)
The Model of that view:
public class MainModel : TranslatableStrings
{
private string helloText, howareuText;
public string HelloText
{
get { return this.helloText; }
set { SetProperty(ref this.helloText, value); }
}
public string HowAreYouText
{
get { return this.howareuText; }
set { SetProperty(ref this.howareuText, value); }
}
}
And the base class of the Model is just a contractual class since it has no implementation, but a base type:
public abstract class TranslatableStrings : BindableBase { }
Then, the View data context is the following one:
public class MainViewModel : BaseViewModel
{
private ObservableCollection<MainViewListRscs> languages = new ObservableCollection<MainViewListRscs>();
private ICommand changeLang;
private MainModel model = new MainModel();
public MainViewModel()
{
Languages = new ObservableCollection<MainViewListRscs>()
{
new MainViewListRscs { LanguageCode = "es-ES", LanguageName = "Español" },
new MainViewListRscs { LanguageCode = "en-EN", LanguageName = "English" },
new MainViewListRscs { LanguageCode = "fr-FR", LanguageName = "Français" },
new MainViewListRscs { LanguageCode = "de-DE", LanguageName = "Deutsch" }
};
}
public ICommand ChangeLanguage
{
get { return changeLang = changeLang ?? new DelegateCommand(OnChangeLanguageRequested); }
}
public ObservableCollection<MainViewListRscs> Languages
{
get { return this.languages; }
set
{
this.languages = value;
OnPropertyChanged();
}
}
public MainViewListRscs SelectedLanguage { get; set; }
public MainModel Model
{
get { return this.model; }
set { this.model = value; }
}
private void OnChangeLanguageRequested()
{
Logger.Debug("MAINVIEW", SelectedLanguage.LanguageName + " selected.");
TranslateManager.UpdateStrings<TranslatableStrings>(SelectedLanguage.LanguageCode, this.Model);
}
public override Task OnNavigatedFrom(NavigationEventArgs args)
{
return null;
}
public override Task OnNavigatedTo(NavigationEventArgs args)
{
return null;
}
}
And the TranslateManager:
public class TranslateManager
{
public async static void UpdateStrings<T>(string langCode, T instance) where T : TranslatableStrings
{
//Get all the classes that implement TranslatableStrings
var currentAssembly = instance.GetType().GetTypeInfo().Assembly;
var translatableClasses = currentAssembly.DefinedTypes.Where(type => type.BaseType == typeof(T)).ToList();
//Open RESX file
ResourceLoader resx = ResourceLoader.GetForCurrentView(langCode);
foreach(var Class in translatableClasses)
{
foreach(var property in Class.DeclaredProperties)
{
string value = resx.GetString(property.Name);
var vmProp = instance.GetType().GetTypeInfo().GetDeclaredProperty(property.Name);
vmProp.SetValue(instance, value);
}
}
}
}
I have achieved changing the two TextBlocks of the MainView but not the view in another project. What I would need to do is to get a list of assemblies contained in a solution. I guess that getting just this would make everything work since I'm using a generic implementation.
Any suggestion will be much appreciated.
Thanks!
Your translation files are loaded as resources. So you can access them anywhere, even in other projects by doing something like
private ResourceLoader _resourceLoader = new ResourceLoader();
var someTranslation =_resourceLoader.GetString("your_localization_key");
Wrap this code nicely into a lib so that you can have an easy access to it from everywhere, and there you go !
I'm trying to set binding on a TapGestureRecognizer in code and I can't figure out the right way to do it. The working xaml looks something like this...
<Grid>
<Grid.GestureRecognizers>
<TapGestureRecognizer Command="{Binding LaunchLocationDetailsCommand}"
CommandParameter="{Binding}" />
</Grid.GestureRecognizers>
</Grid>
And in C#, it looks something like this...
var gridTap = new TapGestureRecognizer();
gridTap.SetBinding(TapGestureRecognizer.CommandProperty,
new Binding("LaunchLocationDetailsCommand"));
gridTap.SetBinding(TapGestureRecognizer.CommandParameterProperty,
new Binding(/* here's where I'm confused */));
var grid = new Grid();
grid.GestureRecognizers.Add(gridTap);
My confusion comes in on the binding of CommandParameterProperty. In xaml, this simply {Binding} with no other parameter. How is this done in code? Passing in new Binding() or this.BindingContext don't seem to work.
The CommandProperty binding is the same as you was doing.
As you are not passing in a path to some property to use, your CommandParameterProperty can't just create an empty Binding as it will throw an exception.
To get around this you need to specify the Source property as Adam has pointed out.
Note, however if the BindingContext you are trying to assign is Null, which I suspect it is in your case, this will still throw an exception.
The Grid in the example below has the BindingContext set to the view model (objGrid.BindingContext = objMyView2).
We are then creating a new binding for our command parameter, with the Source pointing back to our view model (Source = objGrid.BindingContext).
If you run the demo below, you will see a debug message in the Output window indicating a property value from the view model.
MyView2 objMyView2 = new MyView2();
objMyView2.SomeProperty1 = "value1";
objMyView2.SomeProperty2 = "value2";
objMyView2.LaunchLocationDetailsCommand_WithParameters = new Command<object>((o2)=>
{
LaunchingCommands.LaunchLocationDetailsCommand_WithParameters(o2);
});
Grid objGrid = new Grid();
objGrid.BindingContext = objMyView2;
objGrid.HeightRequest = 200;
objGrid.BackgroundColor = Color.Red;
TapGestureRecognizer objTapGestureRecognizer = new TapGestureRecognizer();
objTapGestureRecognizer.SetBinding(TapGestureRecognizer.CommandProperty, new Binding("LaunchLocationDetailsCommand_WithParameters"));
Binding objBinding1 = new Binding()
{
Source = objGrid.BindingContext
};
objTapGestureRecognizer.SetBinding(TapGestureRecognizer.CommandParameterProperty, objBinding1);
//
objGrid.GestureRecognizers.Add(objTapGestureRecognizer);
Supporting classes:-
MyView2:-
public class MyView2
: ViewModelBase
{
public string SomeProperty1 { get; set; }
public string SomeProperty2 { get; set; }
public ICommand LaunchLocationDetailsCommand_WithParameters { get; set; }
}
LaunchingCommands:-
public static class LaunchingCommands
{
public static void LaunchLocationDetailsCommand_WithParameters(object pobjObject)
{
System.Diagnostics.Debug.WriteLine("SomeProperty1 = " + (pobjObject as MyView2).SomeProperty1);
}
}
ViewModelBase:-
public abstract class ViewModelBase
: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string pstrPropertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(pstrPropertyName));
}
}
}
If you have a {Binding} with nothing inside it, it is binding to the binding context and passing that through. Hence you bind it to the default binding context of the page.
When you create a new Binding set the source.
var binding = new Xamarin.Forms.Binding() { Source = this.BindingContext };