关于模板控件如何实现多数据源绑定的问题

在读Clinglingboy的asp.net控件开发基础(18)时,Clinglingboy对其进行了重点讲解。可是我感觉在如何将具有IListSource接口的数据源最终转化为DataView说的还不是十分清楚,下面我这一部分再详细的说一下。
首先还是贴一下关键的DataSourceHelper类

ContractedBlock.gif ExpandedBlockStart.gif DataSourceHelper
None.gifpublic class DataSourceHelper
ExpandedBlockStart.gifContractedBlock.gif    
dot.gif{
InBlock.gif        
public static object ResolveDataSource(object dataSource, string dataMember)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
ContractedSubBlock.gifExpandedSubBlockStart.gif            
如果数据源为空,则返回空值#region 如果数据源为空,则返回空值
InBlock.gif
InBlock.gif            
if (dataSource == null)
InBlock.gif                
return null;
InBlock.gif
ExpandedSubBlockEnd.gif            
#endregion

InBlock.gif
ContractedSubBlock.gifExpandedSubBlockStart.gif            
如果数据源不为空,且为IEnumerable类型,则返回IEnumerable#region 如果数据源不为空,且为IEnumerable类型,则返回IEnumerable
InBlock.gif
InBlock.gif            
if (dataSource is IEnumerable)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
return (IEnumerable)dataSource;
ExpandedSubBlockEnd.gif            }

InBlock.gif
ExpandedSubBlockEnd.gif            
#endregion

InBlock.gif
ContractedSubBlock.gifExpandedSubBlockStart.gif            
如果数据源不为空,且为IListSource类型,则返回IListSource#region 如果数据源不为空,且为IListSource类型,则返回IListSource
InBlock.gif
InBlock.gif            
else if (dataSource is IListSource)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                IList list 
= null;
InBlock.gif                IListSource listSource 
= (IListSource)dataSource;
InBlock.gif                list 
= listSource.GetList();
ContractedSubBlock.gifExpandedSubBlockStart.gif                
判断是否为IList对象集合的值#region 判断是否为IList对象集合的值
InBlock.gif                
if (listSource.ContainsListCollection)
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    
//提供发现可绑定列表架构的功能,其中可用于绑定的属性不同于要绑定到的对象的公共属性
InBlock.gif
                    ITypedList typedList = (ITypedList)list;
InBlock.gif                    
//返回表示用于绑定数据的每项上属性集合
InBlock.gif                    
InBlock.gif                    
//PropertyDescriptorCollection propDescCol =
InBlock.gif                    
//   typedList.GetItemProperties(new PropertyDescriptor[0]);  //was (null)
InBlock.gif
                    PropertyDescriptorCollection propDesCol=new PropertyDescriptorCollection();
InBlock.gif                    
//如果属性说明符数目为0
InBlock.gif
                    if (propDescCol.Count == 0)
InBlock.gif                        
throw new Exception("ListSource without DataMembers");
InBlock.gif
InBlock.gif                    PropertyDescriptor propDesc 
= null;
InBlock.gif
ContractedSubBlock.gifExpandedSubBlockStart.gif                    
判断dataMember字符数给propDesc赋值#region 判断dataMember字符数给propDesc赋值
InBlock.gif                    
//获取属性描述符
InBlock.gif                    
//若不指定dataMember属性则获取默认数据成员
InBlock.gif
                    if ((dataMember == null|| (dataMember.Length < 1))
ExpandedSubBlockStart.gifContractedSubBlock.gif                    
dot.gif{
InBlock.gif                        propDesc 
= propDescCol[0];
ExpandedSubBlockEnd.gif                    }

InBlock.gif                    
else  
InBlock.gif                        
//尝试在属性集合中寻找数据成员
InBlock.gif
                        propDesc = propDescCol.Find(dataMember, true);
InBlock.gif
ExpandedSubBlockEnd.gif                    
#endregion

InBlock.gif
InBlock.gif                    
if (propDesc == null)
InBlock.gif                        
throw new Exception("ListSource missing DataMember");
InBlock.gif                    
InBlock.gif                    
object listitem = list[0];
InBlock.gif
InBlock.gif                    
//获取组件属性当前值
InBlock.gif
                    object member = propDesc.GetValue(listitem);
InBlock.gif
InBlock.gif                    
if ((member == null|| !(member is IEnumerable))
InBlock.gif                        
throw new Exception("ListSource missing DataMember");
InBlock.gif
InBlock.gif                    
return (IEnumerable)member;
ExpandedSubBlockEnd.gif                }

InBlock.gif                
else
InBlock.gif                    
//若不包含Ilist集合,则直接返回
InBlock.gif
                    return (IEnumerable)list;  //robcamer added (IEnumerable)
InBlock.gif

ExpandedSubBlockEnd.gif                
#endregion

ExpandedSubBlockEnd.gif            }

InBlock.gif
ExpandedSubBlockEnd.gif            
#endregion

InBlock.gif            
return null;
InBlock.gif
ExpandedSubBlockEnd.gif        }

ExpandedBlockEnd.gif    }


(1)如果传入的数据源类型是IEnumerable的话,可以直接返回

None.gif              if  (dataSource  is  IEnumerable)
ExpandedBlockStart.gifContractedBlock.gif            
dot.gif {
InBlock.gif                
return (IEnumerable)dataSource;
ExpandedBlockEnd.gif            }

 这里像Array、ArrayList、SqlDataReader、DataView等都直接或者间接的实现了IEnumerable接口。

(2)如果传入的类型非IEnumerable,那么代码会判断数据源是否实现了IListSource接口,因为如果实现了IListSource接口,那么我们同样可以利用此接口的GetList方法返回一个IList,而IList继承IEnumerable,同样可以进行数据绑定。当然如果数据源没有实现IEnumerable和IListSource,数据源就不可绑定。
这里像DataTable、DataSet都实现了IListSource接口。
DataTable实现的GetList方法

None.gif  IList IListSource.GetList()
ExpandedBlockStart.gifContractedBlock.gif 
dot.gif {
InBlock.gif     
return this.DefaultView;
ExpandedBlockEnd.gif }

返回了一个DataView
DataSet实现的GetList方法

None.gif  IList IListSource.GetList()
ExpandedBlockStart.gifContractedBlock.gif 
dot.gif {
InBlock.gif     
return this.DefaultViewManager;
ExpandedBlockEnd.gif }

 

返回了一个DataViewManager。

通过判断IListSource中的ContainsListCollection,我们可以知道包含多个DataTable的DataSet还是只有一个DataTable,对于后者,由于已经通过GetList方法得到了它的DataView,而DataView又实现了IEnumerable接口,问题也解决了。

问题现在集中到如何处理DataSet的数据源,我们来看一下DataViewManager类,除了几个public的属性,还有一个DataViewManagerListItemTypeDescriptor类型的Item值得我们注意,后面会讲解此类。同时DataViewManager类实现了ITypedList接口,接下来利用ITypedList.GetItemProperties(object)得到PropertyDescriptorCollection.



我们看一下ITypedList.GetItemProperties(object)的代码,其中关键一句

return ((ICustomTypeDescriptor) new DataViewManagerListItemTypeDescriptor(this)).GetProperties();

看来DataViewManagerListItemTypeDescriptor的GetProperties方法可以得到PropertyDescriptorCollection。此类是Framework的一个内部类,实现了ICustomTypeDescriptor接口。

那么ICustomTypeDescriptor是做什么用的呢。我们来看一下msdn:

ICustomTypeDescriptor 使对象得以提供有关自身的类型信息。通常,当对象需要动态类型信息时使用此接口。相反,TypeDescriptor 提供从元数据获得的静态类型信息。

大家可能对这句话不太明白,我解释一下,这里我用PropertyGrid举例,不熟悉的可以在网上查,实际上我感觉PropertyGrid在和某个类绑定的时候,默认的是用TypeDescriptor 提供从元数据获得的静态类型信息。如下图

 
但是有些情况,你需要用到 PropertyGrid 去绑定一个属性/值的集合,但是这个属性/值的集合并不适合写成一个固定的类。

比如你想用 PropertyGrid 绑定XML 里的数据。或者数据库的某个表。

假设你有 1000 个XML 文件,每个 XML 所取到的属性集合各不一样,你不可能为每个XML 文件都写一个类 。

或者你的某个数据表有1000 条记录,该表有 a 字段的值表示属性名称, b字段的值表示属性值,你不可能写一个类,定义1000个属性。

这时候,我们就希望是否能够将一个动态的属性/值的集合与Property 绑定。通过实现ICustomTypeDescriptor,我们就可以完成动态的属性/值的集合与Property 绑定。这里参考了PropertyGrid 绑定动态的属性与值的集合文章,这篇文章对大家理解ICustomTypeDescriptor会有很大的帮助,文章的代码是VB2005,我用c#2003重新写了一下,这两段代码我会在文章后面给出下载,建议大家先读这篇文章以帮助理解。我把这篇文章的几个类的关键部分列出来。

ContractedBlock.gif ExpandedBlockStart.gif XProp
None.gifpublic class XProp
ExpandedBlockStart.gifContractedBlock.gif    
dot.gif{
InBlock.gif        
private string theName;
InBlock.gif        
private object theValue;
InBlock.gif        
public string Name
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
get
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
return this.theName;
ExpandedSubBlockEnd.gif            }

InBlock.gif            
set
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
this.theName = value;
ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }

InBlock.gif        
public object Value
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
get
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
return this.theValue;
ExpandedSubBlockEnd.gif            }

InBlock.gif            
set
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
this.theValue = value;
ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }

InBlock.gif        
public override string ToString()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return "Name: " +Name +",Value: "+Value;
ExpandedSubBlockEnd.gif        }

InBlock.gif
InBlock.gif        
public XProp()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
this.theName = "";
InBlock.gif            
this.theValue = null;
ExpandedSubBlockEnd.gif        }

InBlock.gif
ExpandedBlockEnd.gif    }

 

ContractedBlock.gif ExpandedBlockStart.gif XPropDescriptor
None.gif    public class XPropDescriptor:PropertyDescriptor
ExpandedBlockStart.gifContractedBlock.gif    
dot.gif{
InBlock.gif        
private XProp theProp;
InBlock.gif        
public override Type ComponentType
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
get
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
return this.GetType();
ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }

InBlock.gif        
public override bool IsReadOnly
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
get
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
return false;
ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }

InBlock.gif        
public override Type PropertyType
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
get
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
return this.theProp.Value.GetType();
ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }

InBlock.gif        
public XPropDescriptor(XProp prop, Attribute[] attrs) : base(prop.Name, attrs)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
this.theProp = prop;
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public override bool CanResetValue(object component)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return false;
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public override object GetValue(object component)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return this.theProp.Value;
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public override void ResetValue(object component)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public override void SetValue(object component, object value)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
this.theProp.Value = value;
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public override bool ShouldSerializeValue(object component)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return false;
ExpandedSubBlockEnd.gif        }

InBlock.gif
ExpandedBlockEnd.gif    }

 

ContractedBlock.gif ExpandedBlockStart.gif XProps
None.gif    public class XProps:CollectionBase,ICustomTypeDescriptor
ExpandedBlockStart.gifContractedBlock.gif    
dot.gif{
InBlock.gif        
public XProps()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
//
InBlock.gif            
// TODO: 在此处添加构造函数逻辑
InBlock.gif            
//
ExpandedSubBlockEnd.gif
        }

ContractedSubBlock.gifExpandedSubBlockStart.gif        
IList实现#region IList实现
InBlock.gif        
public int Add(XProp prop)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return base.List.Add(prop);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public XProp FindXProp(string name)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            name 
= name.Trim().ToLower();
InBlock.gif            
foreach (XProp prop in base.List)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
if (prop.Name.ToLower() == name)
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    
return prop;
ExpandedSubBlockEnd.gif                }

ExpandedSubBlockEnd.gif            }

InBlock.gif            
return null;
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public void Insert(int index, XProp prop)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
base.List.Insert(index, prop);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public void Remove(XProp prop)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
base.List.Remove(prop);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public XProp this[int index]
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
get
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
return (XProp) base.List[index];
ExpandedSubBlockEnd.gif            }

InBlock.gif            
set
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
base.List[index] = value;
ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }

ExpandedSubBlockEnd.gif        
#endregion

InBlock.gif
ContractedSubBlock.gifExpandedSubBlockStart.gif        
ICustomTypeDescriptor实现#region ICustomTypeDescriptor实现
InBlock.gif        
public AttributeCollection GetAttributes()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetAttributes(thistrue);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public string GetClassName()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetClassName(thistrue);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public string GetComponentName()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetClassName(thistrue);
ExpandedSubBlockEnd.gif        }

InBlock.gif
InBlock.gif        
public TypeConverter GetConverter()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetConverter(thistrue);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public EventDescriptor GetDefaultEvent()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetDefaultEvent(thistrue);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public PropertyDescriptor GetDefaultProperty()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetDefaultProperty(thistrue);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public object GetEditor(Type editorBaseType)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetEditor(this, editorBaseType, true);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public EventDescriptorCollection GetEvents()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetEvents(thistrue);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public EventDescriptorCollection GetEvents(Attribute[] attributes)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetEvents(this, attributes, true);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public PropertyDescriptorCollection GetProperties()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return TypeDescriptor.GetProperties(thistrue);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            PropertyDescriptor[] props 
= new PropertyDescriptor[this.Count + 1];
InBlock.gif            
int count = this.Count - 1;
InBlock.gif            
for (int i = 0; i <= count; i++)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                props[i] 
= new XPropDescriptor(this[i], attributes);
ExpandedSubBlockEnd.gif            }

InBlock.gif            
return new PropertyDescriptorCollection(props);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
public object GetPropertyOwner(PropertyDescriptor pd)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
return this;
ExpandedSubBlockEnd.gif        }

ExpandedSubBlockEnd.gif        
#endregion

InBlock.gif        
public override string ToString()
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            StringBuilder sbld 
= new StringBuilder();
InBlock.gif            
int count = this.Count - 1;
InBlock.gif            
for (int i = 0; i <= count; i++)
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                sbld.Append(
"[" + i + "" + this[i].ToString() + "\r\n");
ExpandedSubBlockEnd.gif            }

InBlock.gif            
return sbld.ToString();
ExpandedSubBlockEnd.gif        }

InBlock.gif
ExpandedBlockEnd.gif    }

回到问题上来,在我们实现了ICustomTypeDescriptor,不需要和PropertyGrid绑定,我们可以得到一个PropertyDescriptorCollection。那么就来具体看看对比。

其中上文的XProp -->  DataTable

                 XProps 的GetProperties方法--> ((ITypedList) DataViewManager).GetItemProperties方法

                 XPropDescriptor--> DataTablePropertyDescriptor

大家会看到((ITypedList) DataViewManager).GetItemProperties方法返回了DataTablePropertyDescriptor的PropertyDescriptorCollection集合;XProps的GetProperties方法返回了XPropDescriptor的PropertyDescriptorCollection集合

在DataTablePropertyDescriptor会有一个DataTable的属性,并且该类复写了GetValue方法,取得值,这个和XPropDescriptor中有XProp属性,且复写了GetValue方法是一致的。唯一不同的是XPropDescriptor的GetValue方法只是将具体的XProp的Value返回,而DataTablePropertyDescriptor中的GetValue方法又利用DataTable进一步操作返回了DataView。

我们现在知道ITypedList.GetItemProperties(object)是怎么得到PropertyDescriptorCollection(确切的说是DataTablePropertyDescriptor),我们接着利用propDesc = propDescCol.Find(dataMember, true)去在集合中查找名字为dataMember值也就是具体的表名,以返回待操作的DataTablePropertyDescriptor。在((ICustomTypeDescriptor) new DataViewManagerListItemTypeDescriptor(this)).GetProperties()方法建立集合的时候采用了表名作为名值对的名,大家可以对照代码看看。接下来再看这段代码

None.gif object  listitem  =  list[ 0 ]; 
None.gif
// 获取组件属性当前值
None.gif
object  member  =  propDesc.GetValue(listitem);

list是什么?实际上是我们在前面得到的DataViewManager.IListSource listSource = (IListSource)dataSource;
list = listSource.GetList();

由于DataViewManager实现了IList接口,因此我们可以用list[index]的形式取得具体的元素,这里我们看到是取得了item的值,还记得我们前面让大家留意DataViewManager的Item属性,实际上它就是一个DataViewManagerListItemTypeDescriptor。propDesc是一个DataTablePropertyDescriptor,来看一下他的GetValue(object)代码

 

None.gif public   override   object  GetValue( object  component)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gifDataViewManagerListItemTypeDescriptor descriptor 
= (DataViewManagerListItemTypeDescriptor) component;
InBlock.gif
return descriptor.GetDataView(this.table);
ExpandedBlockEnd.gif}
 

而DataViewManagerListItemTypeDescriptor的GetDataView的代码 

None.gif internal  DataView GetDataView(DataTable table)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gifDataView view 
= new DataView(table);
InBlock.gifview.SetDataViewManager(
this.dataViewManager);
InBlock.gif
return view;
ExpandedBlockEnd.gif}
 

实际上这一步就是利用DataTable构建DataView,我觉得也可以用其他的方法完成,给DataViewManagerListItemTypeDescriptor增加一个内部的GetDataView方法反而弱化了TypeDescriptor的功能。

到这里,我们就可以返回一个(IEnumberable)DataView了。

PropertyGrid 绑定动态的属性与值的集合文章代码下载:

 

ICustomTypeDescriptorTestVB.zip(VB2005)

ICustomTypeDescriptorTestCSharp.zip(C#2003,其中VB2005使用了范型,改写的时候用了CollectionBase,效果一样)


 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值