Jack Frost
Jack Frost

Reputation: 316

RelayCommand not getting the right Model

I created a user control that looks like a tile. Created another user control named TilePanel that serves as the default container of the tiles. And lastly, the very UI that looks like a Window start screen. I used RelayCommand to bind my TileCommands

Here are the codes:

Tilev2.xaml

<UserControl x:Class="MyNamespace.Tilev2"
             Name="Tile"....
>
...
    <Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" Command="{Binding ElementName=Tile, Path=TileClickCommand}" >
    </Button>

</UserControl>

Tilev2.xaml.cs

    public partial class Tilev2 : UserControl
    {
        public Tilev2()
        {
            InitializeComponent();
        }
//other DPs here
        public ICommand TileClickCommand
        {
            get { return (ICommand)GetValue(TileClickCommandProperty); }
            set { SetValue(TileClickCommandProperty, value); }
        }

        // Using a DependencyProperty as the backing store for TileClickCommand.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TileClickCommandProperty =
            DependencyProperty.Register("TileClickCommand", typeof(ICommand), typeof(Tilev2));
    }
    }

Then I created a TilePanel user control as the container of the tiles

TilePanel.xaml

<UserControl x:Class="MyNamespace.TilePanel"
             ... 
             >
    <Grid>
        <ScrollViewer>
            <ItemsControl Name="tileGroup" 
                          ItemsSource="{Binding TileModels}" >
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Horizontal"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <local2:Tilev2 TileText="{Binding Text}"
                                           TileIcon="{Binding Icon}"
                                           TileSize="{Binding Size}"
                                           TileFontSize="{Binding FontSize}"
                                           Background="{Binding Background}"
                                           TileCaption="{Binding TileCaption}"
                                           TileCaptionFontSize="{Binding TileCaptionFontSize}"
                                           TileClickCommand="{Binding TileCommand}"
                                           />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</UserControl>

TilePanel.xaml.cs

public partial class TilePanel : UserControl
{
    public TilePanel()
    {
        InitializeComponent();
        DataContext = new TilePanelViewModel();
    }

    public TilePanelViewModel ViewModel
    {
        get { return (TilePanelViewModel)this.DataContext; }
    }
}

My ViewModel for TilePanel

TilePanelViewModel.cs

public class TilePanelViewModel : ViewModelBase { private ObservableCollection _tileModels;

public ObservableCollection<TileModel> TileModels
{
    get
    {
        if (_tileModels == null)
            _tileModels = new ObservableCollection<TileModel>();

        return _tileModels;
    }
}

}

Then my Tile model

TileModel.cs

    public class TileModel : BaseNotifyPropertyChanged
    {
        //other members here
        ICommand tileCommand { get; set; }
//other properties here
 public ICommand TileCommand
        {
            get { return tileCommand; }
            set { tileCommand = value; NotifyPropertyChanged("TileCommand"); }
        }
    }
}

This is my StartScreen View where TilePanels with tiles should be displayed...

StartScreen.xaml

<UserControl x:Class="MyNamespace.StartMenu"
             ... >
<Grid>
        <DockPanel x:Name="dockPanel1" Grid.Column="0" Grid.Row="1" Margin="50,5,2,5">
            <local:TilePanel x:Name="tilePanel"></local:TilePanel>
        </DockPanel>
</Grid>
</UserControl>

StartScreen.xaml.cs

    public partial class WincollectStartMenu : UserControl, IView<StartMenuViewModel>
    {
        public WincollectStartMenu()
        {
            InitializeComponent();
        }
public StartMenuViewModel ViewModel { get { return (DataContext as StartMenuViewModel); } }

        private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            ViewModel.Tile = tilePanel.ViewModel.TileModels;
        }

        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            return;
        }
}

In my start screen ViewModel, I used ObservableCollection Tile and use Tile.Add(tile); to populate my start screen with Tiles inside the TilePanel...

StartMenuViewModel.cs

    TileModel tile = new TileModel() { Text = "Testing1", FontSize = 11, Size = TileSize.Medium, Background = (SolidColorBrush)new BrushConverter().ConvertFromString("#039BE5"), Tag="Something" };
    tile.TileCommand = new RelayCommand(
            p => Tile_TileClick(tile.Tag),
            p => true
        );
    temp.Add(tile);

Now the problem is, if I add a new code below, tile = new TileModel() {...} tile.TileCommand = new RelayCommand(...), even if I clicked on the first tile, my Tile_TileClick() will get the second tile's info (or the last tile inserted)...

Am I doing something wrong? Or Im doing everything wrong...?

Upvotes: 0

Views: 90

Answers (2)

Jack Frost
Jack Frost

Reputation: 316

Saw the problem... To pass a parameter from button, I used CommandParameter so I could use it in switch-case scenario to know which button was clicked. But still, param was still null...

<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
        </Button>

TileCommand = new MyCommand() { CanExecuteFunc = param => CanExecuteCommand(), ExecuteFunc = param => Tile_TileClick(param)}

After 2 whole damn days, I changed it:

From this:

<UserControl Name="Tile"...>
    <Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding Tag, ElementName=Tile}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
    </Button>
</UserControl>

To this:

<UserControl Name="Tile"...>
    <Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
    </Button>
</UserControl>

My first post does error because CommandParameter does not know where to get its DataContext so I replaced it to CommandParameter={Binding} so it will get whatever from the DataContext.

Upvotes: 0

Liero
Liero

Reputation: 27360

This is not direct answer to your question, but hopefully it will give you few thoughts.

Ok, first of all, don't name your usercontrol like this:

<UserControl x:Class="MyNamespace.Tilev2" Name="Tile"/>

because the name can be easily overriden when using the usercontrol somewhere:

<local:Titlev2 Name="SomeOtherName" />

and the binding inside Tilevs with ElementName won't work: Command="{Binding ElementName=Tile, Path=TileClickCommand}"


Second, what's the point of Tilev2 usercontrol? Why don't just put the button directly to the DataTemplate inside TilePanel class?

If you need to reuse the template, you can put the template to resource dictionary.

If you need some special presentation code in the Tilev2 codebehind or you need to use the Tilev2 without viewmodel, it's better to create custom control instead of usercontrol in this case. it has much better design time support, and writing control templates it's easier (Triggers, DataTriggers, TempalteBinding, etc). If you used custom Control insead UserControl, you wouldn't have to write {Binding ElementName=Tile, Path=TileClickCommand}, or use RelativeSource, etc.


Third, it seems like you forced MVVM pattern where you can't really take advantage of it. Point of MVVM is separate application logic from presentation. But your Tile and TilePanel usercontrols are just presentation. You application logic could be in StartScreen which is concrete usage of TileName.

I would create custom controls called TilePanel (potentionally inherited from ItemsControl, Selector or ListBox) and if needed also for Tile. Both controls should not be aware of any viewmodels. There's absolutelly no need for that.

Take ListBox as an example. ListBox does not have viewmodel but can be easily used in MVVM scenarios. Just because ListBox it is not tied to any viewmodel, it can be databound to anything.

Just like ListBox creates ListBoxItems, or
Combobox creates ComboBoxItems, or
DataGrid creates DataGridRows or
GridView (in WinRT) creates GridViewRow, your TilePanel could create Tiles.

Bindings to tile specific properties, like Icon or Command could be specified in TilePanel.ItemContainerStyle orusing simillar appriach like DisplayMemberPath, resp ValueMemberPath in ListBox.

final usage could the look like:

<TilePanel ItemsSource="{Bidning ApplicationTiles}" />

or

<TilePanel>
    <Tile Icon=".." Command=".." Text=".." />
    <Tile Icon=".." Command=".." Text=".." />
</TilePanel>

Last, the name `TilePanel' evoked that it is some kind of panel like StackPanel, WrapPanel, etc. In other words, it is FrameworkElement inherited from Panel.

TilesView would be more suitable name for the control than TilePanel. The -View postfix is not from MVVM, it just follows naming convention -GridView, ListView...

Upvotes: 1

Related Questions