Page will not display at all despite no errors - c#

I have been reading my code over and over for the past hour and cannot seem to understand why the changes I have added now don't allow my page to display (the page not displaying is NameEntryPage). The app starts on a the StartPage which has two buttons allowing the user to choose either One Player Game or Two Player Game. If the user chooses Two Player Game, then it navigates to NameEntryPage where the user can enter names for the two players. NameEntryPage has a button that when clicked passes the names entered and navigates to TwoPlayerPage. I have checked StartPage, OnePlayerPage, and TwoPlayerPage and they all still work. However, NameEntryPage does not load anything (and the button on StartPage that takes you there does not work either. To check the other pages I had to change the code in App() to begin on those pages)
App() code
public App()
{
InitializeComponent();
MainPage = new NavigationPage(new Views.NameEntryPage());
}
NameEntryPage.xaml code
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="SampleApp.Views.NameEntryPage">
<ContentPage.Content>
<StackLayout Margin="20">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
<Entry
x:Name="EntryNameP1"
Placeholder="Enter a name for Player 1"
Text="{Binding NameP1}"
Grid.Row="0" Grid.Column="0"/>
<Entry x:Name="EntryNameP2"
Placeholder="Enter a name for Player 2"
Text="{Binding NameP2}"
Grid.Row="1" Grid.Column="0"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Text="Start"/>
</Grid>
</StackLayout>
</ContentPage.Content>
code behind
namespace SampleApp.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NameEntryPage : ContentPage
{
public NameEntryPage()
{
NavigationPage.SetHasNavigationBar(this, false);
InitializeComponent();
BindingContext = new NameEntryPageViewModel(Navigation, EntryNameP1.Text, EntryNameP2.Text);
}
}
}
VM code
public class NameEntryPageViewModel : INotifyPropertyChanged
{
public INavigation Navigation { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string nameP1;
public string NameP1
{
get
{
return NameP1;
}
set
{
if (NameP1 != value)
{
nameP1 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("NameP1"));
}
}
}
private string nameP2;
public string NameP2
{
get
{
return NameP2;
}
set
{
if (NameP2 != value)
{
nameP2 = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("NameP1"));
}
}
}
public Command StartGameCommand { get; set; }
public NameEntryPageViewModel(INavigation navigation, string name1, string name2)
{
nameP1 = name1;
nameP2 = name2;
this.Navigation = navigation;
StartGameCommand = new Command(() => StartGame(nameP1, nameP2));
}
public NameEntryPageViewModel(INavigation navigation)
{
nameP1 = "Player 1";
nameP2 = "Player 2";
this.Navigation = navigation;
StartGameCommand = new Command(() => StartGame(nameP1, nameP2));
}
private void StartGame(string name1, string name2)
{
this.Navigation.PushAsync(new Views.TwoPlayerPage(name1, name2));
}
}

this will create an infinite loop
public string NameP1
{
get
{
return NameP1;
}
NameP1 will call the getter for NameP1, which calls the getter for NameP1, recursively until you crash
instead your getter should return the private nameP1 variable
public string NameP1
{
get
{
return namep1;
}

Related

ViewModel always returns null when trying to access it from code-behind

I am still learning Xamarin.Forms, so please bear with me.
I am developing a control, that let's you take pictures with your camera, displays them in a FlowListView and then ultimately I want to take these pictures, turn them into E-Mail attachments and send them off.
While the Gallery part of the control is working and the E-Mail part was already written by somebody else, for some reason I can't access the control's ViewModel from my E-Mail Code-Behind.
I wrote the control in MVVM, the E-Mail part is written in xaml with code-behind.
Whenever I try to access the ImagePickerViewModel to get the contents of my AttachmentList, I get returned null.
I have already tried not initializing the ImagePickerViewModel in the E-Mail code-behind, it still returns null.
What would be the correct way to do this?
ImagePickerViewModel.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public class ImagePickerViewModel : BindableObject, INotifyPropertyChanged
{
// HERE WOULD BE A TON OF DEPENDENCIES
public ImagePickerViewModel()
{
// Commands
TakePictureCmd = new Command(TakePicture);
//ObservableCollection
TakenPicturesList = new ObservableCollection<Model.ImagePicker>();
AttachmentList = new List<BaseObject>();
}
public event PropertyChangedEventHandler PropertyChanged;
public Model.ImagePicker NewPicture { get; set; }
public BaseObject attachment { get; set; }
public List<BaseObject> AttachmentList { get; set; }
public ObservableCollection<Model.ImagePicker> TakenPicturesList { get; set; }
public string selectedImage { get; set; }
public string Path
{
get { return NewPicture.ImagePath; }
set { NewPicture.ImagePath = value; OnPropertyChanged(nameof(Path)); }
}
public Command TakePictureCmd { get; set; }
private async void TakePicture()
{
var path = await _cameraService.TakePictureAsync();
TakenPicturesList.Add(new Model.ImagePicker { ImagePath = path });
GetFilesForAttachment(path);
}
private void GetFilesForAttachement(string filePath)
{
attachment = Attachment.FromFile(filePath);
if (attachment != null)
{
AttachmentList.Add(attachment);
}
}
public void RaisedOnPropertyChanged(string _PropertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(_PropertyName));
}
}
ImagePickerView.xaml
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:xct="http://xamarin.com/schemas/2020/toolkit"
xmlns:flv="clr-namespace:DLToolkit.Forms.Controls;assembly=DLToolkit.Forms.Controls.FlowListView"
xmlns:ffimageloading="clr-namespace:FFImageLoading.Forms;assembly=FFImageLoading.Forms"
xmlns:local="clr-namespace:SolIT.Mobile.Client"
xmlns:vm="clr-namespace:SolIT.Mobile.Client.Features.Shared.Controls.ImagePicker.ViewModel"
xmlns:buttons="clr-namespace:Syncfusion.XForms.Buttons;assembly=Syncfusion.Buttons.XForms"
x:Class="SolIT.Mobile.Client.Features.Shared.Controls.ImagePicker.View.ImagePickerView"
x:Name="View_ImagePickerView">
<ContentView.BindingContext>
<vm:ImagePickerViewModel ContextView="{x:Reference View_ImagePickerView}"/>
</ContentView.BindingContext>
<Frame>
<ContentView.Content>
<StackLayout>
<Grid
ColumnSpacing="0"
HorizontalOptions="CenterAndExpand">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<buttons:SfButton
x:Name="Btn_takePicture"
Grid.Column="1"
Text="{local:Translate Common.SelectFilesPage.TakePicture}"
TextColor="Black"
ImageSource="{Binding TakePictureIcon}"
ShowIcon="true"
BackgroundColor="Transparent"
ImageAlignment="Top"
HorizontalOptions="FillAndExpand"
Margin="3"
CornerRadius="5"
Command="{Binding TakePictureCmd}"/>
</Grid>
<BoxView Grid.ColumnSpan="2" Grid.Row="1" HeightRequest="1" BackgroundColor="{StaticResource Win10Devider}"/>
<flv:FlowListView x:Name="pictureFlowListView" SeparatorVisibility="None" HasUnevenRows="true"
FlowColumnMinWidth="110" FlowItemsSource="{Binding TakenPicturesList}">
<!--FlowItemTappedCommand="{Binding PictureTappedCmd}"
FlowLastTappedItem="{Binding SelectedItem}"-->
<flv:FlowListView.FlowColumnTemplate>
<DataTemplate>
<Grid Padding="3">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout>
<ffimageloading:CachedImage HeightRequest="100" Aspect="AspectFill"
DownsampleHeight="100" DownsampleUseDipUnits="false" x:Name="imageCached"
Source="{Binding ImagePath}">
<ffimageloading:CachedImage.GestureRecognizers>
<TapGestureRecognizer Command="{Binding Path=BindingContext.PictureTappedCmd, Source={x:Reference pictureFlowListView}}" CommandParameter="{x:Reference imageCached}"></TapGestureRecognizer>
</ffimageloading:CachedImage.GestureRecognizers>
</ffimageloading:CachedImage>
</StackLayout>
</Grid>
</DataTemplate>
</flv:FlowListView.FlowColumnTemplate>
</flv:FlowListView>
</StackLayout>
</ContentView.Content>
</Frame>
</ContentView>
EmailCreatePage.xaml.cs
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class EmailCreatePage: ContentPage
{
// HERE WOULD BE A TON OF DEPENDENCIES
public EmailCreateViewModel ViewModel { get; }
public ImagePickerViewModel ViewModelImagePicker {get;}
public EmailCreatePage()
{
InitializeComponent();
BindingContext = ViewModel = new EmailCreateViewModel();
ViewModelImagePicker = new ImagePickerViewModel();
}
private void CreateAttachment()
{
var attachmentList = GetFileList();
foreach (var file in attachmentList)
{
var attachment = Attachment.FromFile(file.ToString());
if (attachment != null)
{
ViewModel.CubesMessage.ConnectedObjects.Add(attachment);
}
}
public List<BaseObject> GetFileList()
{
// GET LIST FROM OTHER VIEWMODEL HERE
// VIEWMODEL IS ALWAYS NULL
var attachmentList = ImagePickerViewModel.AttachmentList;
return attachmentList;
}
// HERE WOULD BE THE CODE THAT SENDS THE EMAIL OFF AFTER A BUTTON CLICK
From the following code you posted , we find that you want to access variable AttachmentList from ImagePickerViewModel. But here, you just create a new instance of class ImagePickerViewModel, so the attachmentList will always be null.
ViewModelImagePicker = new ImagePickerViewModel();
Besides, you also use code ImagePickerViewModel.AttachmentList; to access variable AttachmentList in ImagePickerViewModel, that's confusing. Here, you can remove code ViewModelImagePicker = new ImagePickerViewModel();.
public List<BaseObject> GetFileList()
{
// GET LIST FROM OTHER VIEWMODEL HERE
// VIEWMODEL IS ALWAYS NULL
var attachmentList = ImagePickerViewModel.AttachmentList;
return attachmentList;
}
In fact, if you want to get the value of variable AttachmentList in class ImagePickerViewModel from other page, you can define a global static varible AttachmentList.
Just as follows:
public stactic List<BaseObject> AttachmentList { get; set; }
Note: Make sure AttachmentList in class ImagePickerViewModel could been assigned values correctly.

Navigate to another tab item by a button click from the tab having multiple view models doesn't work

I have to navigate to another tab by a button click from the first tab in a WPF MVVM Application(c#).
I am trying to achieve this by adding binding to Selected Index property in tab control.There are two different view models are used in the first tab.After adding binding to Selected Index property in tab control it loses the rest of view model's access and No data is present on the text boxes in the first tab. Also navigation is not working . how can I use tab navigation if the window has multiple view models. please see sample code.
XAML file
MainWindow.xaml
<Grid>
<TabControl SelectedIndex="{Binding SelectedTab,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
DataContext="{Binding processVM}">
<TabItem Header="Home">
<Grid ShowGridLines="false" >
<Grid.ColumnDefinitions >
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<TextBox Name="txtCustomerName"
Grid.Row="0" Grid.Column="1"
Text="{Binding CustomerName}"
DataContext="{Binding customerVM}"></TextBox>
<TextBox Name="txtDepositAmount"
Grid.Row="1" Grid.Column="1"
Text="{Binding DepositAmount}"
DataContext="{Binding customerVM}"></TextBox>
<Button Content="Click" Width="100" Height="50"
Grid.Row="2"
DataContext="{Binding processVM}"
Command="{Binding ButtonCommand}"
/>
</Grid>
Code behind
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel()
{
processVM = new ProcessViewModel(),
customerVM = new CustomerViewModel()
};
}
}
View Models
MainWindowViewModel.cs
class MainWindowViewModel
{
public ProcessViewModel processVM { get; set; }
public CustomerViewModel customerVM { get; set; }
}
ProcessViewModel.cs
public class ProcessViewModel: INotifyPropertyChanged
{
private string depositAmount;
public string DepositAmount
{
get { return depositAmount; }
set {
depositAmount = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("DepositAmount"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private ICommand m_ButtonCommand;
public ICommand ButtonCommand
{
get
{
return m_ButtonCommand;
}
set
{
m_ButtonCommand = value;
}
}
private int selectedTab;
public int SelectedTab
{
get { return selectedTab; }
set
{
selectedTab = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("SelectedTab"));
}
}
public ProcessViewModel()
{
ButtonCommand = new RelayCommand(new Action<object>(clickbutton));
depositAmount = "450";
}
public void clickbutton(object obj)
{
MessageBox.Show("clicked");
SelectedTab = 1;
}
}
CustomerViewModel.cs
class CustomerViewModel: ProcessViewModel, INotifyPropertyChanged
{
private string customerName;
public string CustomerName
{
get { return customerName; }
set { customerName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("CustomerName"));
}
}
public CustomerViewModel()
{
CustomerName = "Alex";
}
public event PropertyChangedEventHandler PropertyChanged;
}
Before adding binding for selected index there were no issues.
Your problem is that you're setting TabControl.DataContext. DataContext is inherited, so all the controls inside it are now using processVM as their binding source instead of MainWindowViewModel.
Instead of setting TabControl.DataContext, change the SelectedIndex binding to this:
SelectedIndex="{Binding processVM.SelectedTab, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"

ListView not refreshing after adding value in Xamarin Forms

What i am doing is passing data through more than 2 pages. I assign viewmodel to next page while i am navigating. In second page i have a listview that is not refreshing/updating after adding a value.
Help me please!!
Here is my code
MyViewModel
public class MyViewModel : INotifyPropertyChanged
{
public string _userName { get; set; }
public List<family> familyList;
public List<family> FamilyList
{
get { return familyList; }
set
{
familyList = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public MyViewModel()
{
_userName = "Mak";
familyList = new List<family>();
}
public void AddMember(string memberName)
{
FamilyList.Add(new family
{
name = memberName,
id = Guid.NewGuid().ToString(),
username=_userName
});
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
userdetails.xaml
<cl:BasePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="familyinfo.userdetails" xmlns:cl="clr-namespace:familyinfo;assembly=familyinfo">
<Label Font="Roboto-Medium" FontSize="14" Text="{Bindinbg _userName}" />
<Button Clicked="Next_Step" HeightRequest="30" HorizontalOptions="FillAndExpand" BorderRadius="12" Text="NEXT" />
</cl:BasePage>
userdetails.xaml.cs
public partial class userdetails : BasePage
{
public MyViewModel _myViewModel { get; set; }
public userdetails()
{
InitializeComponent();
BindingContext = new MyViewModel();
}
void Next_Step(object sender, System.EventArgs e)
{
_myViewModel =(MyViewModel) this.BindingContext;
var familyMember = new FamilyMember();
familyMember.BindingContext = _myViewModel;
Application.Current.MainPage = new NavPage(registerCar);
}
}
FamilyMember.xaml
<cl:BasePage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="familyinfo.FamilyMember" xmlns:cl="clr-namespace:familyinfo;assembly=familyinfo">
<Label Font="Roboto-Medium" FontSize="14" Text="{Bindinbg _userName}" />
<cl:CustomEntry x:Name="txtMemberName" Placeholder="Member Name" FontSize="12" />
<Button Clicked="AddMember" HeightRequest="30" HorizontalOptions="FillAndExpand" BorderRadius="12" Text="Add" />
<ListView ItemsSource="{Binding FamilyList}" VerticalOptions="FillAndExpand" BackgroundColor="Transparent">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<ViewCell.View>
<Grid Padding="20,10,0,0" ColumnSpacing="12" RowSpacing="0" BackgroundColor="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto">
</ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto">
</RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Row="0" Text="{Binding name}" Grid.Column="0" Font="Roboto-Medium" FontSize="14" TextColor="#000000" />
</Grid>
</ViewCell.View>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</cl:BasePage>
FamilyMember.xaml.cs
public partial class FamilyMember : BasePage
{
public MyViewModel _myViewModel { get; set; }
public userdetails()
{
InitializeComponent();
}
void AddMember(object sender, System.EventArgs e)
{
_myViewModel = (MyViewModel)this.BindingContext;
_myViewModel.AddMember(txtMemberName.Text);
}
}
I agree with Atul: Using an ObservableCollection is the right way to do it.
A workaround - if you don't have a chance to change that - is to set the ListView's ItemSource to null and back to the list, whenever data changed and the UI needs to update:
void UpdateListView(ListView listView)
{
var itemsSource = listView.ItemsSource;
listView.ItemsSource = null;
listView.ItemsSource = itemsSource;
}
In fact, you must use a collection that implements INotifyCollectionChanged interface (instead of the well known INofifyPropertyChanged). And that's exactly what does ObservableCollection<T> for you. This is why it works like "magic".
I just used ObservableCollection instead of List and it works!!

How to set the item for a CarouselView from code behind?

I have a CarouselView which is bound to an ItemsSource of images.
But I want to change the current Image shown by changing the index of the CarouselView.
I have tried using CarouselView.Position to the index of the element that has to be selected. But unfortunately this does not work.
How can I achieve this?
Thanks
I have tried using CarouselView.Position to the index of the element that has to be selected. But unfortunately this does not work.
Since you're using data binding for ItemsSource of CarouselView, you can implement the INotifyPropertyChanged interface for your image model.
For example:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:cv="clr-namespace:Xamarin.Forms;assembly=Xamarin.Forms.CarouselView"
x:Class="FormsIssue2.Page1">
<Grid>
<cv:CarouselView ItemsSource="{Binding Zoos, Mode=OneWay}" x:Name="CarouselZoos">
<cv:CarouselView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Grid.RowSpan="2" Aspect="AspectFill" Source="{Binding ImageUrl, Mode=OneWay}" />
<StackLayout Grid.Row="1" BackgroundColor="#80000000" Padding="12">
<Label TextColor="White" Text="{Binding Name, Mode=OneWay}" FontSize="16" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" />
</StackLayout>
</Grid>
</DataTemplate>
</cv:CarouselView.ItemTemplate>
</cv:CarouselView>
</Grid>
</ContentPage>
Code behind:
public partial class Page1 : ContentPage
{
public Page1()
{
InitializeComponent();
Zoos = new ObservableCollection<Zoo>
{
new Zoo
{
ImageUrl = "http://wallpaper-gallery.net/images/image/image-13.jpg",
Name = "Woodland Park Zoo"
},
new Zoo
{
ImageUrl = "https://s3-us-west-1.amazonaws.com/powr/defaults/image-slider2.jpg",
Name = "Cleveland Zoo"
},
new Zoo
{
ImageUrl = "http://i.stack.imgur.com/WCveg.jpg",
Name = "Phoenix Zoo"
}
};
//CarouselZoos.ItemsSource = Zoos;
this.BindingContext = this;
CarouselZoos.ItemSelected += CarouselZoos_ItemSelected;
}
private void CarouselZoos_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as Zoo;
if (item == null)
return;
item.ImageUrl = "https://3.bp.blogspot.com/-W__wiaHUjwI/Vt3Grd8df0I/AAAAAAAAA78/7xqUNj8ujtY/s1600/image02.png";
}
public ObservableCollection<Zoo> Zoos { get; set; }
}
public class Zoo : INotifyPropertyChanged
{
private string _ImageUrl;
public string ImageUrl
{
get { return _ImageUrl; }
set
{
if (value != _ImageUrl)
{
_ImageUrl = value;
OnPropertyChanged("ImageUrl");
}
}
}
private string _Name;
public string Name
{
get { return _Name; }
set
{
if (value != _Name)
{
_Name = value;
OnPropertyChanged("Name");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Once an item is selected, you can find this item's instance in SelectedItemChangedEventArgs, then you can change the image source of this item.
Update:
Based on our discussion, I think the item sources for your CarouselView for thumbnails and the CarouselView for your larger images are in the same sequential order, then when you select the item in your thumbnails, you can get the position of thumbnails and scroll the CarouselView for larger images like this:
var postion = CarouselThunbnails.Position;
CarouselImages.Position = postion;

Best practice: How to get custom user input in WPF

Please allow me to present a simplified version of my problem:
Lets say I have a main window called MainWindow in which I would like to display some Person objects in MainWindow. Now, in order to instantiate these Person objects I need a bunch of different fields such as name, age, profession, favourite food, etc...
Here is my solution:
I try to get all input fields and instantiate a Person in a secondary window and then send back the result to the main form.
MainWindow has a public method as follows:
public void (Person input)
{
// use the fields in input to add details to window
}
I have another window in the project called PersonInput that takes in its constructor a reference to a MainWindow and saves it in a field.
private MainWindow owner;
public PersonInput(MainWindow parent)
{
InitializeComponent();
owner = parent;
}
PersonInput has a number of input fields corresponding to the required fields of a Person object.
in addition it has a button called "AddPerson" with an associated onClick event handler as follows: (pseudoCode)
private void button_Click(object sender, RoutedEventArgs e)
{
//get all fields from this form..
String enteredName = this.nameText.Text;
//get more fields....
Person p = new Person(...);
//owner is MainWindow, send Back the Person so details can be displayed
owner.addPerson(p);
this.Close();
}
as you would expect, MainWindow has a Button named "AddPersonButton" which has an on click event handler like this:
private void button_Click(object sender, RoutedEventArgs e)
{
PersonInput x = new PersonInput(this); //pass this as a reference
//so this window can send us back the result when they have it
x.Show(); //open the child window so user can enter information
}
While this method works, I am quite convinced it is not the best practice way to do it. I would like to learn the idiomatic .net WPF way of doing this. Please enlighten me
PersonInput.xaml.cs
public class PersonInput : Window
{
public void PersonInput()
{
InitializeComponent();
Owner = Application.Current.MainWindow;
}
public static Person ShowDialog(Person initializer)
{
var vm = new PersonViewModel(initializer);
var dlg = new PersonInput() { DataContext = vm };
if (dlg.ShowDialog().GetValueOrDefault(false))
{
return vm.ToPerson();
}
return null;
}
private void OK_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}
PersonInputViewModel.cs
public class PersonViewModel : ViewModelBase
{
public PersonViewModel(Person person = null)
{
if (person != null)
{
// Assuming Person has FirstName and LastName properties
FirstName = person.FirstName;
LastName = person.LastName;
// etc. etc. for all the rest
}
}
public Person ToPerson()
{
return new Person()
{
FirstName = this.FirstName,
LastName = this.LastName,
// etc. etc. for all other properties
};
}
private string _firstName = null;
public string FirstName {
get { return _firstName; }
set {
if (value != _firstName) {
_firstName = value;
OnPropertyChanged(nameof(FirstName));
}
}
}
}
ViewModelBase.cs
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
PersonInput.xaml
<Window xmlns:blahblahblah="Blah blah blah" etc etc
Title="Person"
Height="640"
Width="480"
ShowInTaskbar="False"
ResizeMode="CanResizeWithGrip"
WindowStartupLocation="CenterOwner"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="180" Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0">First Name</Label>
<TextBox
Grid.Row="0"
Grid.Column="1"
Text="{Binding FirstName}"
/>
<Label Grid.Row="1" Grid.Column="0">Last Name</Label>
<TextBox
Grid.Row="1"
Grid.Column="1"
Text="{Binding LastName}"
/>
<StackPanel
Orientation="Horizontal"
Grid.Column="1"
Grid.Row="10"
HorizontalAlignment="Right"
>
<Button Content="_OK" Click="OK_Click" />
<Button Content="_Cancel" Click="Cancel_Click" />
</StackPanel>
</Grid>
</Window>
The biggest weakness of this approach is the strong coupling between MainWindow and PersonInput.
A slightly better approach would be to use Observer Pattern and have the Main Window anonymously subscribe.
Simple solution example code:
public interface IAddPersonObserver
{
void OnPersonAdded(Person person);
}
public interface IAddPersonObservable
{
void Subscribe(IAddPersonObserver observer);
void Unsubscribe(IAddPersonObserver observer);
}
public class MainWindow : IAddPersonObserver
{
...
private void button_Click(object sender, RoutedEventArgs e)
{
PersonInput x = new PersonInput();
x.Subscribe(this);
x.Show();
}
public void OnPersonAdded(Person addedPerson)
{
addPerson(addedPerson); // or whatever view update code you want
}
}
Further improvements would revolve around separating the MainWindow from knowing what view gets created or shown, and having an intermediary object (such as a PersonRepository) store/hold/provide the important business data. This is much better than having the application data actually live inside the Views and Application Windows.
Another use MVVM practice:
Solution structure
public class Person {
public int Id { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }
}
public class PersonListViewModel : DependencyObject {
public ObservableCollection<Person> Items { get; set; }
public Person CurrentPerson
{
get { return (Person)GetValue(CurrentPersonProperty); }
set { SetValue(CurrentPersonProperty, value); }
}
public static readonly DependencyProperty CurrentPersonProperty = DependencyProperty.Register("CurrentPerson", typeof(Person), typeof(PersonListViewModel));
public ICommand AddCommand { get; set; }
public ICommand EditCommand { get; set; }
public PersonListViewModel() {
Items = new ObservableCollection<Person>();
AddCommand = new RelayCommand(p=> add() );
EditCommand = new RelayCommand(p=> { return CurrentPerson != null; }, p => edit());
}
private void add() {
Person p= new Person();
p.Id = Items.Count();
p.Name = "New Name";
p.Birthday = DateTime.Now;
Items.Add(p);
}
private void edit() {
var viewModel = new PersonItemViewModel(CurrentPerson);
var view = new View.PersonEditWindow();
view.DataContext = viewModel;
view.Show();
}
}
public class PersonItemViewModel : DependencyObject {
Person person;
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly DependencyProperty NameProperty = DependencyProperty.Register("Name", typeof(string), typeof(PersonItemViewModel) );
public DateTime Birthday
{
get { return (DateTime)GetValue(BirthdayProperty); }
set { SetValue(BirthdayProperty, value); }
}
public static readonly DependencyProperty BirthdayProperty = DependencyProperty.Register("Birthday", typeof(DateTime), typeof(PersonItemViewModel));
public PersonItemViewModel() {
}
public PersonItemViewModel(Person source) {
this.person = source;
Name = person.Name;
Birthday = person.Birthday;
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) {
base.OnPropertyChanged(e);
if (e.Property == NameProperty) {
person.Name = (string) e.NewValue;
}
if (e.Property == BirthdayProperty) {
person.Birthday = (DateTime)e.NewValue;
}
}
}
List Form:
<Window x:Class="WpfApplication1.View.PersonListWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1.View"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Title="PersonListWindow" Height="300" Width="300"
xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"
d:DataContext="{d:DesignInstance Type=viewModel:PersonListViewModel, IsDesignTimeCreatable=True}"
>
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<DataGrid ItemsSource="{Binding Items}" SelectedItem="{Binding CurrentPerson, Mode=TwoWay}">
</DataGrid>
<StackPanel Grid.Row="1" Height="32" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Margin="4" Command="{Binding AddCommand}">Add</Button>
<Button Margin="4" Command="{Binding EditCommand}">Edit</Button>
</StackPanel>
</Grid>
Edit Form:
<Window x:Class="WpfApplication1.View.PersonEditWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1.View"
mc:Ignorable="d"
Title="PersonEditWindow" Height="300" Width="300"
xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"
d:DataContext="{d:DesignInstance Type=viewModel:PersonItemViewModel, IsDesignTimeCreatable=True}"
>
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0">Name</TextBlock>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Name}"></TextBox>
<TextBlock Grid.Row="1">Birthday</TextBlock>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Birthday}"></TextBox>
</Grid>
Results (Image)

Categories

Resources