我正在开发一个业务应用程序,使用Silverlight作为UI,使用WCF Web服务作为后端。在数据库中,我有许多查找表。当WCF服务返回业务对象时,其中一个属性包含查找表中的整行而不仅仅是外键,因此在UI中我可以显示查找表中的描述之类的内容,而无需再次调用服务。我现在要做的是提供一个绑定到整个查找值列表的组合框并让它正确更新。我在这个例子中处理的业务对象叫做Session,查询叫做SessionType。
以下是组合框的定义。 DataContext设置为Session的实例。我正在设置一个ItemTemplate,因为组合框显示的不仅仅是一个字符串列表。
<ComboBox
x:Name="SessionTypesComboBox"
ItemTemplate="{StaticResource SessionTypeDataTemplate}"
ItemsSource="{Binding Source={StaticResource AllSessionTypes}}"
SelectedItem="{Binding Path=SessionType, Mode=TwoWay}"
/>
业务对象和查找表都是通过Web服务异步加载的。如果我什么都不做,组合框列表将填充SessionTypes,但它不会显示Session的初始SessionType值。但是,如果更改了组合框选择,则会使用正确的SessionType更新会话。
似乎正在发生的事情是SelectedItem绑定无法将Session中的SessionType与SessionType列表中的等效项匹配。对象值相同但引用不是。
我找到的解决方法是加载Session和SessionTypes列表,然后使用SesstionTypes列表中相应的SessionType更新Session的当前SessionType。如果我这样做,那么组合框会正确显示。然而对我来说这有一个糟糕的代码味道。因为所有内容都是异步加载的,所以我必须确定所有内容何时可用。我正是这样做的:
在我的Silverlight用户控件的代码隐藏中:
// incremented every time we get data back during initial form load.
private volatile int m_LoadSequence = 0;
...
// Loaded event, called when the form is er... loaded.
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
// load session types
var sessionTypes = this.Resources["AllSessionTypes"] as Lookups.AllSessionTypes;
if (sessionTypes != null)
{
sessionTypes.DataLoadCompleted += (s, ea) =>
{
IncrementLoadSequence();
};
sessionTypes.LoadAsync();
}
// start loading another lookup table, same as above
// omitted for clarity
// set our DataContect to our business object (passed in when form was created)
this.LayoutRoot.DataContext = this.m_Session;
IncrementLoadSequence();
}
// This is the smelly part. This gets called by OnBlahCompleted events as web service calls return.
private void IncrementLoadSequence()
{
// check to see if we're expecting any more service calls to complete.
if (++m_LoadSequence < 3)
return;
// set lookup values on m_Session to the correct one in SessionType list.
// Get SessionType list from page resources
var sessionTypes = this.Resources["AllSessionTypes"] as Lookups.AllSessionTypes;
// Find the matching SessionType based on ID
this.m_Session.SessionType = sessionTypes.Where((st) => { return st.SessionTypeID == this.m_Session.SessionType.SessionTypeID; }).First();
// (other lookup table omitted for clarity)
}
所以基本上我有一个计数器,每次从webservice获取数据时都会增加。因为我期待3件事(核心业务对象+ 2个查找表),当该计数器达到3时,我匹配引用。
对我来说,这看起来非常hacky。我宁愿看到组合框指定一个ValueMemberPath和SelectedValue来匹配所选项目与列表中的一个。
有人能看到更清洁的方式吗?这种情况在商业应用程序中很常见,所以我确信必须有一种很好的方法。
答案 0 :(得分:2)
杰夫,
确认我理解你的问题:数据绑定基础设施似乎没有认识到你认为'等于'的两个对象实际上是相等的 - 因此最初的{{ 1}}未正确设置,因为数据绑定未在SelectedItem
集合中找到与_ {1}}匹配的reference-equals对象。
你通过'展平'引用来解决这个问题(即你强制 StaticResource
是Session.SessionType
代码中的引用等号。
我们有similar problem。
确实有点理解,Silverlight不会自动“等同”来自不同“来源”的两个对象,因为你知道它们代表相同的数据。就像你说的那样“对象值是相同的,但引用不是”。但是,如何让数据绑定等同于它们呢?
我们想到/尝试的事情:
在类上实现.Equals()(在您的情况下为Session.SessionType
)
在课堂上实施operator ==(在您的情况下为Where((st)...First()
)
在课堂上实施IEquatable
(在您的情况下为SessionType
)
仅使集合SessionType
并绑定到字符串属性
但最后我们已经放弃并使用了与您相同的方法 - 从'集合'中“提取”正确的reference-equals对象(在所有内容加载后)并将其戳入SessionType
- 约束财产。
我同意你的代码气味,并怀疑必须是一个更好的解决方案。到目前为止,我们在属性访问器和no-op String
中的所有调试都没有找到解决方案 - 但如果我们这样做,我也会在这里发布它。
答案 1 :(得分:1)
我不确定我是否完全理解这个问题(现在很早:) :)但是你不能只在一次通话中转移你需要的所有物品吗? (即使您必须将3包装在新的DTO类中),您也可以使用完整事件更新当前会话类型。它仍然不完美,但至少你不需要保留任何计数器。
我还将所有逻辑移到ViewModel并绑定到那个,但那只是我:)
答案 2 :(得分:1)
最好不要绑定到ObservableCollection,然后使用其他代码(MVVM的View Model部分不是一个糟糕的选择)在后台更新它。通过这种方式,您可以与UI分离,并且在绑定UI时更容易处理更新。
答案 3 :(得分:0)
感谢您的回答,以上所有内容都很有用!我正朝着MVVM方式发展,并将多个服务调用合并为一个(也减少了往返开销)。看起来我暂时会坚持使用查找重新引用 - 如果我找到更好的方式,我也会发布它。
答案 4 :(得分:0)
<强> geofftnz 下, 你找到了解决方案吗?
<强> CraigD 下, 我怀疑压倒等等是一个很好的解决方案。首先,这将在生成的代理类SessionType中完成,因此这些更改将在每个服务引用更新时丢失。其次,SessionType setter中的通知(此处SessionType是相同的生成的客户端代理类)使用ReferenceEquals调用...所以这是触摸生成的代码的另一个地方!好的,第一件事可以通过手工制作的部分类SessionType完成(因此在更新后也不会丢失),但第二件事肯定不能以同样的方式完成。